"""The lease registry — the single source of truth for "what's running".

The warden owns this in memory (authoritative); it's serialized to ``test_status``
for read-visibility and mirrored to ``live/leases.json`` for crash recovery. The web
and CLI *read* it; they never decide run-state from a guess.
"""

from __future__ import annotations

import hashlib
import json
from collections import deque
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any


def now_iso() -> str:
    return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")


def parse_iso(ts: str) -> datetime:
    return datetime.strptime(ts, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)


def age_seconds(then: str, now: str) -> float:
    return (parse_iso(now) - parse_iso(then)).total_seconds()


def compute_config_digest(config: dict[str, Any]) -> str:
    """Stable digest of the effective config — the third axis of single-flight identity."""
    blob = json.dumps(config, sort_keys=True, ensure_ascii=False).encode("utf-8")
    return "sha256:" + hashlib.sha256(blob).hexdigest()


@dataclass(frozen=True)
class RunRequest:
    """A request submitted to the warden. ``key`` is what gets single-flighted."""

    run_id: str
    test_id: str
    config: dict[str, Any] = field(default_factory=dict)
    origin: str = "cli"  # web | cli | agent | ci
    who: str = ""
    independent: bool = False

    @property
    def config_digest(self) -> str:
        return compute_config_digest(self.config)

    def key(self, project: str) -> str:
        # serialize-by-default: one run per project; independent tests get their own lease
        return self.test_id if self.independent else project


@dataclass
class Lease:
    """The right to run, held for the duration of a run (heartbeat + TTL for recovery)."""

    run_id: str
    key: str
    test_id: str
    config_digest: str
    origin: str
    who: str
    acquired_at: str
    heartbeat_at: str
    ttl_s: int
    progress: float = 0.0
    watchers: list[str] = field(default_factory=list)
    #: pid of the run's child process (learned from the runner's ``test_start``); the
    #: crash-recovery journal carries it so a recovering warden can kill the orphan.
    pid: int | None = None

    def to_status(self) -> dict[str, Any]:
        holder: dict[str, Any] = {"origin": self.origin}
        if self.who:
            holder["who"] = self.who
        return {
            "run_id": self.run_id,
            "key": self.key,
            "test_id": self.test_id,
            "holder": holder,
            "acquired_at": self.acquired_at,
            "heartbeat_at": self.heartbeat_at,
            "ttl_s": self.ttl_s,
            "progress": self.progress,
            "watchers": len(self.watchers),
        }


@dataclass
class LeaseRegistry:
    """Running leases (key -> Lease), the FIFO queue, and the rolling audit of refusals."""

    project: str
    serialize: bool = True
    running: dict[str, Lease] = field(default_factory=dict)
    queue: list[RunRequest] = field(default_factory=list)
    recent: deque[dict[str, Any]] = field(default_factory=lambda: deque(maxlen=50))

    def audit(self, event: str, **fields: Any) -> dict[str, Any]:
        entry: dict[str, Any] = {"ts": now_iso(), "event": event, **fields}
        self.recent.append(entry)
        return entry

    def to_status(self, *, broker: str = "up") -> dict[str, Any]:
        """Serialize to the fed-resources ``test_status`` wire format (read-only to the web)."""
        return {
            "schema_version": "0.1",
            "kind": "test_status",
            "project": self.project,
            "broker": broker,
            "serialize": self.serialize,
            "running": [lease.to_status() for lease in self.running.values()],
            "queued": [
                {"run_id": r.run_id, "test_id": r.test_id, "origin": r.origin, "position": i + 1}
                for i, r in enumerate(self.queue)
            ],
            "recent": list(self.recent),
        }
