"""GenerationReceipt — caller-facing record of one dispatch.

Wraps the CP-4 RunResult with provenance the caller cares about:
who dispatched (caller_id), what project/episode/shot, when (timestamp_utc),
and the dispatch_path string the StepRunner stamps onto sidecars.

Schema is locked at CP-5. CP-6 binds receipts to WorkflowSteps; CP-7's Take
wraps a receipt; CP-9's PanelOfJudges fills `eval_scores`. Adding fields
post-CP-5 is safe; renaming/removing is a contract break.

JSON round-trip: GenerationReceipt.from_dict(g.to_dict()) == g.
"""

from __future__ import annotations

import time
from dataclasses import dataclass, field, fields
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional

from recoil.pipeline.core.registry import RunResult


def utc_now_iso8601() -> str:
    """ISO 8601 UTC string with Z suffix (e.g., '2026-04-27T03:14:15Z')."""
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _run_result_to_dict(run_result: RunResult) -> dict[str, Any]:
    """Serialize RunResult without deepcopying arbitrary metadata objects."""
    d = {f.name: getattr(run_result, f.name) for f in fields(RunResult)}
    d["metadata"] = dict(run_result.metadata or {})
    return d


@dataclass(frozen=True)
class GenerationReceipt:
    """Caller-facing record of a single dispatch.

    CP-5 deliverable. Wraps CP-4 RunResult plus dispatch provenance.

    Fields:
        receipt_id: Stable id, format f"rcpt_{epoch_us}_{shot_id}_{modality}".
        modality: Canonical modality string. Mirrors run_result.modality.
        caller_id: Which entry point dispatched. Free-form string but
            recommended values: "production_loop", "pipeline_orchestrator",
            "dispatch_cli", "review_server", "api_routes", "test", "harness".
        project: Project slug (e.g. "tartarus") if known, else None.
        episode: Episode number if applicable, else None.
        shot_id: Canonical shot id (e.g. "EP001_SH02") if applicable, else None.
        timestamp_utc: ISO 8601 UTC.
        run_result: The CP-4 RunResult, byte-for-byte unchanged.
        provenance: Free-form dict — dispatch_path, prompt_hash, sidecar_path,
            model, payload_keys (filtered list), error_class, etc.
        eval_scores: CP-9 fills this. CP-5 always sets `{}`.
    """

    receipt_id: str
    modality: str
    caller_id: str
    project: Optional[str]
    episode: Optional[int]
    shot_id: Optional[str]
    timestamp_utc: str
    run_result: RunResult
    provenance: dict[str, Any] = field(default_factory=dict)
    eval_scores: dict[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        if self.modality != self.run_result.modality:
            raise ValueError(
                f"GenerationReceipt.modality ({self.modality!r}) must match "
                f"run_result.modality ({self.run_result.modality!r})"
            )

    def to_dict(self) -> dict[str, Any]:
        """Serialize for JSON / JSONL emission. RunResult flattened in-line."""
        return {
            "receipt_id": self.receipt_id,
            "modality": self.modality,
            "caller_id": self.caller_id,
            "project": self.project,
            "episode": self.episode,
            "shot_id": self.shot_id,
            "timestamp_utc": self.timestamp_utc,
            "run_result": _run_result_to_dict(self.run_result),
            "provenance": dict(self.provenance),
            "eval_scores": dict(self.eval_scores),
        }

    @classmethod
    def from_dict(cls, d: dict[str, Any]) -> "GenerationReceipt":
        """Inverse of to_dict. Reconstructs RunResult from the nested dict."""
        rr = d["run_result"]
        rr_field_names = {f.name for f in fields(RunResult)}
        rr_kwargs = {k: v for k, v in rr.items() if k in rr_field_names}
        if "metadata" in rr_kwargs:
            rr_kwargs["metadata"] = dict(rr_kwargs["metadata"] or {})
        run_result = RunResult(**rr_kwargs)
        return cls(
            receipt_id=d["receipt_id"],
            modality=d["modality"],
            caller_id=d["caller_id"],
            project=d.get("project"),
            episode=d.get("episode"),
            shot_id=d.get("shot_id"),
            timestamp_utc=d["timestamp_utc"],
            run_result=run_result,
            provenance=dict(d.get("provenance") or {}),
            eval_scores=dict(d.get("eval_scores") or {}),
        )


def make_receipt_id(shot_id: Optional[str], modality: str) -> str:
    """Stable receipt id format: rcpt_{epoch_us}_{shot_id_or_unknown}_{modality}.

    Microsecond precision avoids collisions when many shots dispatch
    in the same second.
    """
    sid = shot_id or "unknown"
    return f"rcpt_{int(time.time() * 1_000_000)}_{sid}_{modality}"


# ── Reporter helpers ──────────────────────────────────────────────────

def aggregate_disk_truth(receipts) -> int:
    """Count receipts whose output_path exists on disk.

    Bug D mitigation: the harness historically summed `segment_count`
    across r2v_multi receipts, overstating disk reality. Sum these
    per-receipt 1-or-0 values for disk truth instead.

    Args:
        receipts: iterable of GenerationReceipt (or anything with
            .run_result.output_path).

    Returns:
        Integer count of receipts whose output_path exists on disk.
    """
    count = 0
    for r in receipts:
        if r is None:
            continue
        rr = getattr(r, "run_result", None)
        if rr is None:
            continue
        op = getattr(rr, "output_path", None)
        if not op:
            continue
        try:
            if Path(op).exists():
                count += 1
        except (TypeError, ValueError):
            continue
    return count
