"""The read seam — how the console reads a project's warden + fed-resources spine.

``ProjectReader`` is the console's only door onto a project:

* the **warden** (authority) via :class:`WardenClient` over ``testctl._socket(root)``
  — ``status()`` returns the live lease registry, or an honest ``broker:"down"``
  payload when the warden is unreachable (**fail-closed**, never a blank);
* the **spine** (read-only disk) — the gated ``test_catalog.json``, the durable
  ``generated/runs/*.json`` run-records, and the ephemeral ``live/runs/*.ndjson``
  telemetry. All reads are non-mutating and tolerant of missing/garbled files
  (honest-absence: an absent file is ``None``/``[]``, never a 500).
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from .._gitref import head_commit
from ..testctl import _socket
from ..warden import WardenClient, WardenUnreachable


class ProjectReader:
    def __init__(self, root: str | Path) -> None:
        self.root = Path(root)
        self.client = WardenClient(_socket(self.root))

    @property
    def _generated(self) -> Path:
        return self.root / "fed-resources" / "generated"

    @property
    def _live(self) -> Path:
        return self.root / "fed-resources" / "live"

    # ---------------------------------------------------- warden (authority) --
    def status(self) -> dict[str, Any]:
        """The live lease registry, or an honest ``broker:"down"`` (fail-closed)."""
        try:
            return self.client.status()
        except WardenUnreachable:
            return {
                "schema_version": "0.1",
                "kind": "test_status",
                "project": self.root.name,
                "broker": "down",
                "running": [],
                "queued": [],
                "recent": [],
            }

    # ------------------------------------------------------- spine (on disk) --
    def catalog(self) -> dict[str, Any] | None:
        """The gated ``test_catalog.json`` (``None`` if not generated yet)."""
        return self._read_json(self._generated / "test_catalog.json")

    def catalog_staleness(self) -> dict[str, Any] | None:
        """Whether the catalog *may* be stale — its provenance ``source_commit`` vs
        the repo's current HEAD (read passively; no git binary). ``None`` when it
        can't tell (no provenance sidecar, or the commit is unreadable) — the console
        then shows nothing, never a wrong "fresh"/"stale" verdict.
        """
        prov = self._read_json(self._generated / "test_catalog.provenance.json")
        if not prov:
            return None
        generated = str(prov.get("source_commit") or "")
        current = head_commit(self.root)
        if not generated or not current:
            return None
        return {
            "stale": generated != current,
            "generated_commit": generated[:9],
            "current_commit": current[:9],
        }

    def history(self) -> list[dict[str, Any]]:
        """All run-records, newest first (``[]`` if none ran yet)."""
        runs = self._generated / "runs"
        if not runs.is_dir():
            return []
        records: list[dict[str, Any]] = []
        for path in sorted(runs.glob("*.json")):
            record = self._read_json(path)
            if record is not None and record.get("kind") == "test_run_record":
                records.append(record)
        records.sort(key=lambda r: str(r.get("started_at", "")), reverse=True)
        return records

    def live_events(self, run_id: str | None) -> list[dict[str, Any]]:
        """The ephemeral NDJSON log lines for one run (``[]`` if absent)."""
        if not run_id:
            return []
        path = self._live / "runs" / f"{run_id}.ndjson"
        if not path.is_file():
            return []
        events: list[dict[str, Any]] = []
        for line in path.read_text(encoding="utf-8").splitlines():
            stripped = line.strip()
            if not stripped:
                continue
            try:
                event = json.loads(stripped)
            except json.JSONDecodeError:
                continue
            if isinstance(event, dict):
                events.append(event)
        return events

    @staticmethod
    def _read_json(path: Path) -> dict[str, Any] | None:
        if not path.is_file():
            return None
        try:
            data = json.loads(path.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError):
            return None
        return data if isinstance(data, dict) else None
