"""Persistent, thread-safe background job state."""

from __future__ import annotations

import threading
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable

from dashboard.persistence import atomic_write_json, load_json

_ACTIVE = {"queued", "running"}


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


class JobRegistry:
    """Reserve jobs before spawning and persist their lifecycle."""

    def __init__(self, path: str | Path, *, history_limit: int = 100):
        self.path = Path(path)
        self.history_limit = history_limit
        self._lock = threading.RLock()
        loaded = load_json(self.path, {"jobs": []}) or {"jobs": []}
        self._jobs = {
            job["id"]: dict(job)
            for job in loaded.get("jobs", [])
            if isinstance(job, dict) and job.get("id")
        }
        changed = False
        for job in self._jobs.values():
            if job.get("status") in _ACTIVE:
                job.update(
                    status="interrupted",
                    finished_at=_now(),
                    error="process restarted before job completed",
                )
                changed = True
        if changed:
            self._save_locked()

    def _save_locked(self) -> None:
        jobs = sorted(self._jobs.values(), key=lambda job: job.get("created_at", ""))
        if len(jobs) > self.history_limit:
            keep = jobs[-self.history_limit :]
            self._jobs = {job["id"]: job for job in keep}
            jobs = keep
        atomic_write_json(self.path, {"jobs": jobs})

    def reserve(self, kind: str, *, details: dict[str, Any] | None = None) -> dict[str, Any] | None:
        with self._lock:
            if any(
                job.get("kind") == kind and job.get("status") in _ACTIVE
                for job in self._jobs.values()
            ):
                return None
            job = {
                "id": uuid.uuid4().hex,
                "kind": kind,
                "status": "queued",
                "created_at": _now(),
                "started_at": None,
                "finished_at": None,
                "progress": {},
                "details": details or {},
                "result": None,
                "error": None,
            }
            self._jobs[job["id"]] = job
            self._save_locked()
            return dict(job)

    def get(self, job_id: str) -> dict[str, Any] | None:
        with self._lock:
            job = self._jobs.get(job_id)
            return dict(job) if job else None

    def active(self, kind: str) -> dict[str, Any] | None:
        with self._lock:
            for job in self._jobs.values():
                if job.get("kind") == kind and job.get("status") in _ACTIVE:
                    return dict(job)
        return None

    def update_progress(self, job_id: str, progress: dict[str, Any]) -> None:
        with self._lock:
            job = self._jobs[job_id]
            job["progress"] = dict(progress)
            self._save_locked()

    def run(self, job_id: str, operation: Callable[[], Any]) -> Any:
        with self._lock:
            job = self._jobs[job_id]
            if job["status"] != "queued":
                raise RuntimeError(f"job {job_id} is not queued")
            job.update(status="running", started_at=_now())
            self._save_locked()
        try:
            result = operation()
        except Exception as exc:
            with self._lock:
                job.update(status="error", error=str(exc), finished_at=_now())
                self._save_locked()
            raise
        with self._lock:
            job.update(status="complete", result=result, finished_at=_now())
            self._save_locked()
        return result
