"""ADR-0161 Step 1 — Read-only queue projection."""

from __future__ import annotations

import json
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import Any

from teaching.proposals import ProposalLog, ReviewState


@dataclass(frozen=True, slots=True)
class QueueItem:
    proposal_id: str
    source_kind: str
    source_id: str | None
    proposed_chain: dict[str, Any]
    replay_evidence: dict[str, Any] | None
    state: ReviewState
    review_history: tuple[dict[str, Any], ...]
    contemplation_report_path: str | None
    age_proposals: int


@lru_cache(maxsize=1)
def _load_contemplation_mapping(runs_dir: Path, runs_dir_mtime: float) -> dict[str, str]:
    """Cache the mapping of proposal_id to JSON file path under runs_dir.

    Keyed on runs_dir and its modification time (mtime) for invalidation.
    """
    mapping: dict[str, str] = {}
    if runs_dir.exists() and runs_dir.is_dir():
        for path in runs_dir.glob("*.json"):
            if not path.is_file():
                continue
            try:
                with path.open("r", encoding="utf-8") as f:
                    data = json.load(f)
            except Exception:
                continue

            if not isinstance(data, dict):
                continue

            pids: set[str] = set()
            report_kind = data.get("report_kind", "learning_arc")
            if report_kind == "learning_arc":
                top_pid = data.get("proposal_id")
                if isinstance(top_pid, str):
                    pids.add(top_pid)

                scenes = data.get("scenes")
                if isinstance(scenes, list):
                    for scene in scenes:
                        if isinstance(scene, dict):
                            detail = scene.get("detail")
                            if isinstance(detail, dict):
                                scene_pid = detail.get("proposal_id")
                                if isinstance(scene_pid, str):
                                    pids.add(scene_pid)
            elif report_kind == "queue_full":
                candidates_skipped = data.get("candidates_skipped", [])
                if isinstance(candidates_skipped, list):
                    for cand in candidates_skipped:
                        if isinstance(cand, dict):
                            cand_id = cand.get("candidate_id")
                            if isinstance(cand_id, str):
                                pids.add(cand_id)

            for pid in pids:
                mapping[pid] = str(path.resolve())
    return mapping


def derive_queue(
    log: ProposalLog,
    contemplation_runs_dir: Path,
) -> tuple[QueueItem, ...]:
    """Derive a read-only queue projection from the ProposalLog.

    Order: FIFO by first-pending-event order in the log.
    """
    events = log.events()
    proposals_data: dict[str, dict[str, Any]] = {}
    created_order: list[str] = []

    for ev in events:
        kind = ev.get("event")
        if kind == "created":
            p = ev.get("proposal") or {}
            pid = p.get("proposal_id")
            if not pid:
                continue
            if pid not in proposals_data:
                created_order.append(pid)
                source_dict = p.get("source") or {}
                source_kind = source_dict.get("kind", "")
                source_id = source_dict.get("source_id")
                # Normalize empty string to None
                if source_id == "":
                    source_id = None

                proposals_data[pid] = {
                    "proposal_id": pid,
                    "source_kind": source_kind,
                    "source_id": source_id,
                    "proposed_chain": p.get("proposed_chain"),
                    "replay_evidence": p.get("replay_evidence"),
                    "state": p.get("review_state", "pending"),
                    "review_history": [],
                }
        elif kind == "replay":
            pid = ev.get("proposal_id")
            if pid in proposals_data:
                proposals_data[pid]["replay_evidence"] = ev.get("replay_evidence")
        elif kind == "transition":
            pid = ev.get("proposal_id")
            if pid in proposals_data:
                proposals_data[pid]["state"] = ev.get("to")
                # Append transition event to review_history
                proposals_data[pid]["review_history"].append(dict(ev))

    # Retrieve cached mapping using runs_dir mtime
    try:
        mtime = contemplation_runs_dir.stat().st_mtime
    except OSError:
        mtime = 0.0

    contemplation_mapping = _load_contemplation_mapping(contemplation_runs_dir, mtime)

    # Build the final tuple of QueueItem
    items: list[QueueItem] = []
    for i, pid in enumerate(created_order):
        data = proposals_data[pid]
        state = data["state"]

        if state == "pending":
            # Per ADR-0161 §4, age is subsequent proposals appended regardless of state.
            age_proposals = len(created_order) - 1 - i
        else:
            age_proposals = 0

        item = QueueItem(
            proposal_id=pid,
            source_kind=data["source_kind"],
            source_id=data["source_id"],
            proposed_chain=data["proposed_chain"],
            replay_evidence=data["replay_evidence"],
            state=state,
            review_history=tuple(data["review_history"]),
            contemplation_report_path=contemplation_mapping.get(pid),
            age_proposals=age_proposals,
        )
        items.append(item)

    return tuple(items)


def find_queue_item(
    log: ProposalLog,
    proposal_id: str,
    contemplation_runs_dir: Path,
) -> QueueItem | None:
    """Find a specific queue item by exact ID or unique prefix."""
    items = derive_queue(log, contemplation_runs_dir)
    for item in items:
        if item.proposal_id == proposal_id:
            return item

    matches = [item for item in items if item.proposal_id.startswith(proposal_id)]
    if len(matches) == 1:
        return matches[0]
    return None
