from __future__ import annotations

import sqlite3
from collections.abc import Callable
from datetime import datetime
from typing import Any
from zoneinfo import ZoneInfo

from backend.database.connection import Database
from backend.jobs.repository import JobRepository, public_job

SHANGHAI = ZoneInfo("Asia/Shanghai")


class JobAlreadyRunning(RuntimeError):
    pass


class JobService:
    def __init__(self, database: Database, repository: JobRepository) -> None:
        self._database = database
        self._repository = repository

    def execute(
        self,
        *,
        kind: str,
        run_key: str,
        requested_date: str,
        trigger: str,
        operation: Callable[[], dict[str, Any]],
        stale_after_seconds: int,
    ) -> dict[str, Any]:
        started = datetime.now(SHANGHAI)
        try:
            with self._database.transaction() as connection:
                row = self._repository.begin(
                    connection,
                    kind=kind,
                    run_key=run_key,
                    requested_date=requested_date,
                    trigger=trigger,
                    started_at=started,
                    stale_after_seconds=stale_after_seconds,
                )
        except sqlite3.IntegrityError as exc:
            raise JobAlreadyRunning("同类任务正在运行") from exc
        run_id = int(row["id"])
        try:
            result = operation()
        except Exception as exc:
            finished = datetime.now(SHANGHAI)
            with self._database.transaction() as connection:
                self._repository.fail(
                    connection,
                    run_id,
                    finished_at=finished,
                    duration_ms=_duration(started, finished),
                    error_code=type(exc).__name__,
                    error_message=_exception_message(exc),
                )
            raise
        finished = datetime.now(SHANGHAI)
        coverage = result.get("coverage")
        with self._database.transaction() as connection:
            self._repository.finish(
                connection,
                run_id,
                finished_at=finished,
                duration_ms=_duration(started, finished),
                coverage=float(coverage) if isinstance(coverage, (int, float)) else None,
                source_set=[str(value) for value in result.get("source_set") or []],
                output_version=str(result.get("output_version") or ""),
                payload=result,
            )
        return result

    def latest(self, limit: int = 40) -> list[dict[str, Any]]:
        with self._database.read() as connection:
            return [public_job(row) for row in self._repository.latest(connection, limit)]

    def latest_for_kind(self, kind: str) -> dict[str, Any] | None:
        with self._database.read() as connection:
            row = self._repository.latest_for_kind(connection, kind)
        return public_job(row) if row else None

    def latest_success(self, kind: str, requested_date: str = "") -> dict[str, Any] | None:
        with self._database.read() as connection:
            row = self._repository.latest_success(connection, kind, requested_date)
        return public_job(row) if row else None

    def ready_for_schedule(
        self,
        kind: str,
        *,
        now: datetime,
        completed_after_seconds: int,
        failed_after_seconds: int,
    ) -> bool:
        """Return whether a scheduler may start another run of this job kind."""
        latest = self.latest_for_kind(kind)
        if latest is None:
            return True
        if latest["status"] == "running":
            return False
        reference = latest.get("finished_at") or latest.get("started_at")
        if not reference:
            return True
        try:
            elapsed = (now - datetime.fromisoformat(str(reference))).total_seconds()
        except ValueError:
            return True
        cooldown = (
            completed_after_seconds
            if latest["status"] == "completed"
            else failed_after_seconds
        )
        return elapsed >= cooldown


def _duration(started: datetime, finished: datetime) -> int:
    return max(0, round((finished - started).total_seconds() * 1000))


def _exception_message(error: Exception) -> str:
    message = getattr(error, "message", None)
    if isinstance(message, str) and message.strip():
        return message.strip()
    return str(error).strip() or "任务执行失败"
