from __future__ import annotations

from datetime import datetime
from typing import Any, Dict, List, Literal, Mapping, Optional, Sequence, Tuple

from pydantic import AliasChoices

from receipts.compat.pyd import BaseModel, ConfigDictLike, Field, model_validator

from metrics.builders import CalibrationBuilder, CoherenceBuilder, DignityBuilder
from metrics.models import CalibrationMetrics, CoherenceMetrics, DignityMetrics
from receipts.witness import Proof, ProofOfImpossibility

_DEFAULT_PROPHECY_THRESHOLDS = {
    "calibration_ece": 0.03,
    "dignity_floor": 0.80,
    "evhd_rho": 0.80,
    "coherence_score": 0.85,
}


def _coerce_float(value: Any) -> Optional[float]:
    try:
        candidate = float(value)
    except (TypeError, ValueError):
        return None
    if isinstance(candidate, float) and candidate != candidate:  # NaN check
        return None
    return candidate


def _evaluate_witness(value: Optional[float], threshold: Optional[float], comparator: str) -> Optional[bool]:
    if value is None or threshold is None:
        return None
    if comparator == "<=":
        return value <= threshold
    if comparator == ">=":
        return value >= threshold
    if comparator == "==":
        return value == threshold
    if comparator == "!=":
        return value != threshold
    return None


def _compute_gap(value: Optional[float], threshold: Optional[float], comparator: str) -> Tuple[Optional[float], Optional[float]]:
    if value is None or threshold is None:
        return (None, None)
    if comparator == "<=":
        return max(0.0, value - threshold), threshold - value
    if comparator == ">=":
        return max(0.0, threshold - value), value - threshold
    if comparator == "==":
        diff = abs(value - threshold)
        return diff, -diff
    if comparator == "!=":
        return (0.0 if value != threshold else threshold, None)
    return (None, None)


def _witness_payload(
    label: str,
    value: Optional[float],
    threshold: Optional[float],
    comparator: str,
    *,
    extra: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    gap, margin = _compute_gap(value, threshold, comparator)
    passed = _evaluate_witness(value, threshold, comparator)
    payload: Dict[str, Any] = {
        "label": label,
        "value": value,
        "threshold": threshold,
        "comparator": comparator,
        "passed": bool(passed) if passed is not None else False,
        "gap": gap,
        "margin": margin,
    }
    if extra:
        payload.update(extra)
    return payload


def _fetch_metric_slice(
    metrics: Any,
    required: Sequence[str],
    *,
    aliases: Sequence[str] = (),
    optional: Sequence[str] = (),
) -> Optional[Dict[str, Any]]:
    """Extract a candidate metric slice from legacy dict payloads."""

    if not isinstance(metrics, Mapping):
        return None

    candidate_blocks: List[Mapping[str, Any]] = [metrics]
    for alias in aliases:
        nested = metrics.get(alias)
        if isinstance(nested, Mapping):
            candidate_blocks.append(nested)

    for block in candidate_blocks:
        if all(key in block for key in required):
            subset: Dict[str, Any] = {key: block[key] for key in required}
            for key in optional:
                if key in block:
                    subset[key] = block[key]
            return subset
    return None


def _maybe_build_calibration(
    existing: Optional[CalibrationMetrics],
    metrics: Any,
) -> Optional[CalibrationMetrics]:
    if existing is not None:
        return existing
    slice_data = _fetch_metric_slice(
        metrics,
        ("ece", "brier", "coverage", "n"),
        aliases=("calibration", "calibration_metrics"),
    )
    if not slice_data:
        return None
    try:
        return CalibrationBuilder(slice_data).finalize()
    except ValueError:
        return None


def _maybe_build_coherence(
    existing: Optional[CoherenceMetrics],
    metrics: Any,
) -> Optional[CoherenceMetrics]:
    if existing is not None:
        return existing
    slice_data = _fetch_metric_slice(
        metrics,
        ("delta", "tol"),
        aliases=("coherence", "coherence_metrics"),
    )
    if not slice_data:
        return None
    try:
        return CoherenceBuilder(slice_data).finalize()
    except ValueError:
        return None


def _maybe_build_dignity(
    existing: Optional[DignityMetrics],
    metrics: Any,
) -> Optional[DignityMetrics]:
    if existing is not None:
        return existing
    slice_data = _fetch_metric_slice(
        metrics,
        ("dwf",),
        aliases=("dignity", "dignity_metrics"),
        optional=("components",),
    )
    if not slice_data:
        return None
    try:
        return DignityBuilder(slice_data).finalize()
    except ValueError:
        return None

from ops.opa.version import OPA_POLICY_BUNDLE_SHA256, OPA_POLICY_VERSION
from receipts.base import (
    BaseReceipt,
    CoherenceStatus,
    GateStatus,
    PolicyWitness,
    RecourseItem,
    UncertaintyLineage,
    PAOutcomeSLA,
)


class ProphecyStone(BaseReceipt):
    """Prediction receipt with gate compliance."""

    receipt_type: str = "ProphecyStone"
    asset_id: str = Field(
        default="",
        description="Asset identifier (e.g., patient_id)",
        validation_alias=AliasChoices("asset_id", "game_id"),
    )
    pred_prob: float = Field(
        description="Predicted probability (primary canonical name)",
        validation_alias=AliasChoices("pred_prob", "prediction_value", "prediction_prob"),
    )
    ci90_lo: float = Field(
        description="Lower bound of the 90% interval (canonical)",
        validation_alias=AliasChoices("ci90_lo", "ci_lower", "ci_low"),
    )
    ci90_hi: float = Field(
        description="Upper bound of the 90% interval (canonical)",
        validation_alias=AliasChoices("ci90_hi", "ci_upper", "ci_high"),
    )
    # Accept a couple of legacy/alternate names commonly used in tests and fixtures
    # so round-trip parsing remains stable across schema variants.
    gates_passed: List[str] = Field(
        default_factory=list,
        description="Legacy alias for passed gate list",
        validation_alias=AliasChoices("gates_passed", "gates_passed"),
    )
    witnesses: Dict[str, Any] = Field(
        default_factory=dict,
        description="Legacy witnesses block (hydrated into metrics when possible)",
        validation_alias=AliasChoices("witnesses", "witnesses"),
    )
    policy_witnesses: List[Any] = Field(
        default_factory=list,
        description="Legacy list of policy witness snapshots",
        validation_alias=AliasChoices("policy_witnesses", "policy_witnesses"),
    )
    ci90_method: Optional[str] = None
    ci90_fallback_reason: Optional[str] = None
    ci_fallback_mode: Optional[str] = Field(
        default=None,
        description="Name of the fallback strategy used to widen the interval.",
    )
    market_prior: Optional[float] = None
    lam_used: Optional[float] = None
    lambda_blend: Optional[float] = Field(
        default=None,
        description="Blend weight used in logit space: logit(p*) = (1-λ)·logit(p_data) + λ·logit(p_prior)",
    )
    p_prior: Optional[float] = Field(
        default=None,
        description="Market prior probability (devigged odds or options-implied move)",
    )
    # Support legacy naming for target fields used in some fixtures/tests
    target_name: Optional[str] = Field(
        default=None,
        description="Canonical target name (accepts legacy 'prediction_target')",
        validation_alias=AliasChoices("target_name", "prediction_target"),
    )
    target_value: Optional[float] = Field(
        default=None,
        description="Canonical target value (accepts legacy 'prediction_value')",
        validation_alias=AliasChoices("target_value", "prediction_value"),
    )
    prior_snapshot_hash: Optional[str] = Field(
        default=None,
        description="SHA-256 of JCS-canonicalized market prior snapshot (odds, timestamp, source)",
    )
    sigma: Optional[float] = None
    dwf: Optional[float] = None
    policy_bundle_hash: str = Field(default=OPA_POLICY_BUNDLE_SHA256)
    policy_version: str = Field(default=OPA_POLICY_VERSION)
    gatebook_hash: Optional[str] = None
    policy_bundle_receipt_id: Optional[str] = None
    gatebook_receipt_id: Optional[str] = None
    reasons: List[str] = Field(default_factory=list)
    policy_reasons: List[str] = Field(default_factory=list)
    metrics: Dict[str, Any] = Field(default_factory=dict)
    calibration_typed: Optional[CalibrationMetrics] = Field(
        default=None,
        description="Typed calibration metrics hydrated from legacy dict payloads.",
    )
    coherence_typed: Optional[CoherenceMetrics] = Field(
        default=None,
        description="Typed coherence metrics hydrated from legacy dict payloads.",
    )
    dignity_typed: Optional[DignityMetrics] = Field(
        default=None,
        description="Typed dignity metrics hydrated from legacy dict payloads.",
    )
    run_context: Dict[str, Any] = Field(default_factory=dict)
    gate_status: Optional[GateStatus] = None
    parent_id: Optional[str] = None
    slate_id: Optional[str] = None
    active_signals: List[str] = Field(default_factory=list)
    applied_rules: List[str] = Field(default_factory=list)
    rails_triggered: List[str] = Field(default_factory=list)
    flow_now: Optional[float] = None
    flow_regime: Optional[str] = None
    temp_used: Optional[float] = None
    p_hat_raw: Optional[float] = None
    target_name: Optional[str] = None
    target_value: Optional[float] = None
    bypass_gates: bool = False
    reliability_icon_path: Optional[str] = None
    dignity_vector: Optional[Dict[str, Any]] = None
    memory_hits: List[Dict[str, Any]] = Field(
        default_factory=list,
        description="Memory records that influenced this prophecy (redacted per privacy mode)",
    )
    memory_selection_hash: Optional[str] = Field(
        default=None,
        description="SHA-256 of sorted memory hit IDs for deterministic replay (C2)",
    )
    memory_status: Optional[str] = Field(
        default=None,
        description="Memory injection status: active, bypassed, emergency_stopped, or blocked",
    )
    policy_guard: Optional[Dict[str, Any]] = Field(
        default=None,
        description="Normalized LOS policy guard evidence (digest, reasons, witness) captured at prediction time",
    )
    policy_witness: Optional[PolicyWitness] = Field(
        default=None,
        description="Machine-verifiable policy compliance witness (gatebook digest, verdict, latency, kid).",
    )
    cohort_explanation: Optional[Dict[str, Any]] = Field(
        default=None,
        description="K-anonymised cohort explanation block (counts, rates, metrics, receipt link).",
    )
    coherence_scores: Optional[Dict[str, Any]] = None
    coherence: Optional[CoherenceStatus] = None
    scd_result: Optional[Dict[str, Any]] = None

    # Lineage tracking (Tier 1B)
    dip_parent: Optional[str] = Field(
        default=None,
        description="Receipt ID of the parent DIPReceipt that sourced this prophecy (Tier 1B lineage)",
    )
    dip_parent_hash: Optional[str] = Field(
        default=None,
        description="SHA-256 hash of the parent DIPReceipt for integrity verification",
    )
    thread_id: Optional[str] = Field(
        default=None,
        description="Thread identifier linking related receipts (DIP → Prophecy → Outcome)",
    )

    @model_validator(mode="after")
    def _hydrate_typed_metrics(self) -> "ProphecyStone":
        metrics_block = self.metrics or {}
        object.__setattr__(
            self,
            "calibration_typed",
            _maybe_build_calibration(self.calibration_typed, metrics_block),
        )
        object.__setattr__(
            self,
            "coherence_typed",
            _maybe_build_coherence(self.coherence_typed, metrics_block),
        )
        object.__setattr__(
            self,
            "dignity_typed",
            _maybe_build_dignity(self.dignity_typed, metrics_block),
        )
        return self

    @model_validator(mode="before")
    @classmethod
    def _coerce_legacy_aliases(cls, data: Any) -> Any:
        # Normalize legacy/alternate field names used in fixtures/tests into
        # the canonical field names used by this model.
        if not isinstance(data, dict):
            return data
        payload = dict(data)

        # asset/game id
        if "game_id" in payload and "asset_id" not in payload:
            payload["asset_id"] = payload.pop("game_id")

        # prediction target/value -> target_name/target_value or pred_prob
        if "prediction_target" in payload and "target_name" not in payload:
            payload["target_name"] = payload.pop("prediction_target")
        if "prediction_value" in payload:
            # If pred_prob present prefer that; otherwise map to pred_prob
            if "pred_prob" not in payload and "target_value" not in payload:
                payload["pred_prob"] = payload.pop("prediction_value")
            else:
                payload.setdefault("target_value", payload.pop("prediction_value"))

        # ci_lower/ci_upper -> ci90_lo/ci90_hi
        if "ci_lower" in payload and "ci90_lo" not in payload:
            payload["ci90_lo"] = payload.pop("ci_lower")
        if "ci_upper" in payload and "ci90_hi" not in payload:
            payload["ci90_hi"] = payload.pop("ci_upper")

        # legacy witness/gates fields -> keep but stored on canonical names
        if "gates_passed" in payload and "gates_passed" not in payload:
            payload.setdefault("gates_passed", payload.pop("gates_passed"))
        if "witnesses" in payload:
            payload.setdefault("witnesses", payload.get("witnesses"))
        if "policy_witnesses" in payload:
            payload.setdefault("policy_witnesses", payload.get("policy_witnesses"))

        return payload

    def _resolve_thresholds(self) -> Dict[str, float]:
        metrics_block = self.metrics or {}
        raw_thresholds = {
            "calibration_ece": _coerce_float(metrics_block.get("calibration_ece_threshold")),
            "dignity_floor": _coerce_float(metrics_block.get("dignity_floor")),
            "evhd_rho": _coerce_float(metrics_block.get("evhd_threshold")),
            "coherence_score": _coerce_float(metrics_block.get("coherence_threshold")),
        }
        resolved: Dict[str, float] = {}
        for key, default in _DEFAULT_PROPHECY_THRESHOLDS.items():
            candidate = raw_thresholds.get(key)
            resolved[key] = candidate if candidate is not None else default
        return resolved

    def _ensure_witness_block(self) -> Dict[str, Dict[str, Any]]:
        metrics_block = self.metrics or {}
        existing = metrics_block.get("witnesses")
        if isinstance(existing, Mapping):
            return dict(existing)

        thresholds = self._resolve_thresholds()
        witnesses: Dict[str, Dict[str, Any]] = {}

        ece_value = (
            _coerce_float(metrics_block.get("worst_slice_ece"))
            or _coerce_float(metrics_block.get("ece_worst"))
            or _coerce_float(metrics_block.get("ece"))
        )
        if ece_value is None and self.calibration_typed is not None:
            ece_value = _coerce_float(getattr(self.calibration_typed, "worst_slice_ece", None))
        if ece_value is not None:
            witnesses["calibration"] = _witness_payload(
                "calibration",
                ece_value,
                thresholds["calibration_ece"],
                "<=",
                extra={"summary": f"Worst-slice ECE {ece_value:.3f} ≤ {thresholds['calibration_ece']:.3f}"},
            )

        dwf_value = _coerce_float(metrics_block.get("dwf"))
        if dwf_value is None:
            dignity_block = metrics_block.get("dignity")
            if isinstance(dignity_block, Mapping):
                dwf_value = _coerce_float(dignity_block.get("dwf") or dignity_block.get("weight"))
        if dwf_value is None and self.dignity_typed is not None:
            dwf_value = _coerce_float(getattr(self.dignity_typed, "weight", None))
        if dwf_value is not None:
            witnesses["dignity"] = _witness_payload(
                "dignity",
                dwf_value,
                thresholds["dignity_floor"],
                ">=",
                extra={"summary": f"Dignity weight {dwf_value:.3f} ≥ {thresholds['dignity_floor']:.3f}"},
            )

        evhd_rho = _coerce_float(metrics_block.get("evhd_rho"))
        if evhd_rho is None:
            evhd_payload = metrics_block.get("evhd_check")
            if isinstance(evhd_payload, Mapping):
                evhd_rho = _coerce_float(evhd_payload.get("rho"))
        if evhd_rho is not None:
            witnesses["evhd"] = _witness_payload(
                "evhd",
                evhd_rho,
                thresholds["evhd_rho"],
                ">=",
                extra={"summary": f"EVHD ρ {evhd_rho:.3f} ≥ {thresholds['evhd_rho']:.3f}"},
            )

        coherence_gap = None
        if "coherence_gap" in metrics_block:
            coherence_gap = _coerce_float(metrics_block.get("coherence_gap"))
            if coherence_gap is not None:
                witnesses["coherence"] = _witness_payload(
                    "coherence",
                    coherence_gap,
                    _coerce_float(metrics_block.get("coherence_gap_threshold")) or 0.01,
                    "<=",
                    extra={"summary": f"Coherence gap {coherence_gap:.4f} ≤ {(_coerce_float(metrics_block.get('coherence_gap_threshold')) or 0.01):.4f}"},
                )
        if coherence_gap is None:
            coherence_score = _coerce_float(metrics_block.get("coherence_score"))
            if coherence_score is None and self.coherence_typed is not None:
                coherence_score = _coerce_float(getattr(self.coherence_typed, "delta", None))
            if coherence_score is not None:
                witnesses["coherence"] = _witness_payload(
                    "coherence",
                    coherence_score,
                    thresholds["coherence_score"],
                    ">=",
                    extra={"summary": f"Coherence score {coherence_score:.3f} ≥ {thresholds['coherence_score']:.3f}"},
                )

        if witnesses:
            metrics_clone = dict(metrics_block)
            metrics_clone["witnesses"] = witnesses
            object.__setattr__(self, "metrics", metrics_clone)
        return witnesses

    @property
    def proof(self) -> Proof:
        witnesses = self._ensure_witness_block()
        return Proof(
            claim="Prophecy satisfies configured gate thresholds",
            evidence=witnesses,
            reasoning="Witness metrics re-evaluated against thresholds",
        )

    def verify(self) -> bool:
        witnesses = self._ensure_witness_block()
        if not witnesses:
            return False
        for payload in witnesses.values():
            comparator = str(payload.get("comparator", ""))
            value = _coerce_float(payload.get("value"))
            threshold = _coerce_float(payload.get("threshold"))
            evaluation = _evaluate_witness(value, threshold, comparator)
            if evaluation is None or not evaluation:
                return False
            if bool(payload.get("passed")) != evaluation:
                return False
        return True

    @property
    def data(self) -> Dict[str, Any]:
        return dict(self.metrics or {})


class RefusalStone(BaseReceipt):
    """Refusal receipt when gates fail."""

    receipt_type: str = "RefusalStone"
    asset_id: str
    reasons: List[str]
    policy_bundle_hash: str = Field(default=OPA_POLICY_BUNDLE_SHA256)
    policy_version: str = Field(default=OPA_POLICY_VERSION)
    gatebook_hash: Optional[str] = None
    policy_bundle_receipt_id: Optional[str] = None
    gatebook_receipt_id: Optional[str] = None
    policy_reasons: List[str] = Field(default_factory=list)
    gate_failures: Dict[str, Any] = Field(default_factory=dict)
    metrics: Dict[str, Any] = Field(default_factory=dict)
    calibration_typed: Optional[CalibrationMetrics] = Field(
        default=None,
        description="Typed calibration metrics hydrated from legacy dict payloads.",
    )
    coherence_typed: Optional[CoherenceMetrics] = Field(
        default=None,
        description="Typed coherence metrics hydrated from legacy dict payloads.",
    )
    dignity_typed: Optional[DignityMetrics] = Field(
        default=None,
        description="Typed dignity metrics hydrated from legacy dict payloads.",
    )
    run_context: Dict[str, Any] = Field(default_factory=dict)
    gate_status: Optional[GateStatus] = None
    parent_id: Optional[str] = None
    slate_id: Optional[str] = None
    active_signals: List[str] = Field(default_factory=list)
    applied_rules: List[str] = Field(default_factory=list)
    rails_triggered: List[str] = Field(default_factory=list)
    flow_now: Optional[float] = None
    flow_regime: Optional[str] = None
    temp_used: Optional[float] = None
    p_hat_raw: Optional[float] = None
    target_name: Optional[str] = None
    target_value: Optional[float] = None
    bypass_gates: bool = False
    dignity_vector: Optional[Dict[str, Any]] = None
    failed_facets: List[str] = Field(default_factory=list)
    recourse_paths: Dict[str, str] = Field(default_factory=dict)
    recourse: List[RecourseItem] = Field(default_factory=list)
    recourse_hint: Optional[str] = Field(
        default=None,
        description="Short operator-facing hint generated by abstention classification.",
    )
    memory_blocked_reason: Optional[str] = Field(
        default=None,
        description="Reason memory injection was blocked (e.g., memory_guard_violation, stale_rate_exceeded)",
    )
    memory_selection_hash: Optional[str] = Field(
        default=None,
        description="SHA-256 of attempted memory selection before blocking",
    )
    coherence_scores: Optional[Dict[str, Any]] = None
    coherence: Optional[CoherenceStatus] = None
    scd_result: Optional[Dict[str, Any]] = None
    refusal_type: Optional[str] = Field(
        default=None,
        description="AbstentionBench-aligned refusal type (soft_decline, specification_gap, etc.)",
    )
    abstention_scenario: Optional[str] = Field(
        default=None,
        description="AbstentionBench scenario mapping (unknown_answer, false_premise, etc.)",
    )
    council_disagreement_std: Optional[float] = Field(
        default=None,
        description="Standard deviation of council/ensemble predictions",
    )
    policy_guard: Optional[Dict[str, Any]] = Field(
        default=None,
        description="Normalized LOS policy guard evidence (digest, reasons, witness) captured at refusal time",
    )
    policy_witness: Optional[PolicyWitness] = Field(
        default=None,
        description="Machine-verifiable policy compliance witness (gatebook digest, verdict, latency, kid).",
    )
    similar_case_refs: List[Dict[str, Any]] = Field(
        default_factory=list,
        description="Retrieved prior cases or reflections relevant to this refusal.",
    )
    reflection_hint: Optional[str] = Field(
        default=None,
        description="Short hint distilled from recent reflections suggesting next best evidence collection step.",
    )
    uncertainty_lineage: Optional[UncertaintyLineage] = Field(
        default=None,
        description="Nearest-neighbour lineage explaining the uncertainty source and actionable asks.",
    )
    cohort_explanation: Optional[Dict[str, Any]] = Field(
        default=None,
        description="K-anonymised cohort explanation block (counts, rates, metrics, receipt link).",
    )

    # Lineage tracking (Tier 1B)
    dip_parent: Optional[str] = Field(
        default=None,
        description="Receipt ID of the parent DIPReceipt that triggered this refusal (Tier 1B lineage)",
    )
    dip_parent_hash: Optional[str] = Field(
        default=None,
        description="SHA-256 hash of the parent DIPReceipt for integrity verification",
    )
    thread_id: Optional[str] = Field(
        default=None,
        description="Thread identifier linking related receipts (DIP → Refusal → Resolution)",
    )

    @model_validator(mode="after")
    def _hydrate_typed_metrics(self) -> "RefusalStone":
        metrics_block = self.metrics or {}
        object.__setattr__(
            self,
            "calibration_typed",
            _maybe_build_calibration(self.calibration_typed, metrics_block),
        )
        object.__setattr__(
            self,
            "coherence_typed",
            _maybe_build_coherence(self.coherence_typed, metrics_block),
        )
        object.__setattr__(
            self,
            "dignity_typed",
            _maybe_build_dignity(self.dignity_typed, metrics_block),
        )
        return self

    @model_validator(mode="before")
    @classmethod
    def _coerce_legacy_aliases(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        payload = dict(data)
        if "game_id" in payload and "asset_id" not in payload:
            payload["asset_id"] = payload.pop("game_id")
        if "prediction_target" in payload and "target_name" not in payload:
            payload["target_name"] = payload.pop("prediction_target")
        if "prediction_value" in payload and "pred_prob" not in payload:
            payload["pred_prob"] = payload.pop("prediction_value")
        if "ci_lower" in payload and "ci90_lo" not in payload:
            payload["ci90_lo"] = payload.pop("ci_lower")
        if "ci_upper" in payload and "ci90_hi" not in payload:
            payload["ci90_hi"] = payload.pop("ci_upper")
        # Map refusal-style names
        if "refusal_reasons" in payload and "reasons" not in payload:
            payload["reasons"] = payload.pop("refusal_reasons")
        if "gates_failed" in payload and "failed_facets" not in payload:
            payload["failed_facets"] = payload.pop("gates_failed")
        if "witnesses" in payload and "metrics" not in payload:
            payload["metrics"] = {"witnesses": payload.pop("witnesses")}
        if "policy_witnesses" in payload and "policy_witness" not in payload:
            pw = payload.pop("policy_witnesses")
            payload["policy_witness"] = pw[0] if isinstance(pw, list) and pw else None
        return payload

    def _resolve_thresholds(self) -> Dict[str, float]:
        metrics_block = self.metrics or {}
        raw_thresholds = {
            "calibration_ece": _coerce_float(metrics_block.get("calibration_ece_threshold")),
            "dignity_floor": _coerce_float(metrics_block.get("dignity_floor")),
            "evhd_rho": _coerce_float(metrics_block.get("evhd_threshold")),
            "coherence_score": _coerce_float(metrics_block.get("coherence_threshold")),
            "coherence_gap": _coerce_float(metrics_block.get("coherence_gap_threshold")),
        }
        resolved: Dict[str, float] = {}
        for key, default in _DEFAULT_PROPHECY_THRESHOLDS.items():
            candidate = raw_thresholds.get(key)
            resolved[key] = candidate if candidate is not None else default
        if raw_thresholds.get("coherence_gap") is not None:
            resolved["coherence_gap"] = raw_thresholds["coherence_gap"]
        else:
            resolved["coherence_gap"] = 0.01
        return resolved

    def _ensure_witness_block(self) -> Dict[str, Dict[str, Any]]:
        metrics_block = self.metrics or {}
        existing = metrics_block.get("witnesses")
        if isinstance(existing, Mapping):
            return dict(existing)

        thresholds = self._resolve_thresholds()
        witnesses: Dict[str, Dict[str, Any]] = {}

        ece_value = (
            _coerce_float(metrics_block.get("worst_slice_ece"))
            or _coerce_float(metrics_block.get("ece_worst"))
            or _coerce_float(metrics_block.get("ece"))
        )
        if ece_value is None and self.calibration_typed is not None:
            ece_value = _coerce_float(getattr(self.calibration_typed, "worst_slice_ece", None))
        if ece_value is not None:
            witnesses["calibration"] = _witness_payload(
                "calibration",
                ece_value,
                thresholds["calibration_ece"],
                "<=",
                extra={"summary": f"Worst-slice ECE {ece_value:.3f} ≤ {thresholds['calibration_ece']:.3f}"},
            )

        dwf_value = _coerce_float(metrics_block.get("dwf"))
        if dwf_value is None:
            dignity_block = metrics_block.get("dignity")
            if isinstance(dignity_block, Mapping):
                dwf_value = _coerce_float(dignity_block.get("dwf") or dignity_block.get("weight"))
        if dwf_value is None and self.dignity_typed is not None:
            dwf_value = _coerce_float(getattr(self.dignity_typed, "weight", None))
        if dwf_value is not None:
            witnesses["dignity"] = _witness_payload(
                "dignity",
                dwf_value,
                thresholds["dignity_floor"],
                ">=",
                extra={"summary": f"Dignity weight {dwf_value:.3f} ≥ {thresholds['dignity_floor']:.3f}"},
            )

        evhd_rho = _coerce_float(metrics_block.get("evhd_rho"))
        if evhd_rho is None:
            evhd_payload = metrics_block.get("evhd_check")
            if isinstance(evhd_payload, Mapping):
                evhd_rho = _coerce_float(evhd_payload.get("rho"))
        if evhd_rho is not None:
            witnesses["evhd"] = _witness_payload(
                "evhd",
                evhd_rho,
                thresholds["evhd_rho"],
                ">=",
                extra={"summary": f"EVHD ρ {evhd_rho:.3f} ≥ {thresholds['evhd_rho']:.3f}"},
            )

        coherence_gap = _coerce_float(metrics_block.get("coherence_gap"))
        if coherence_gap is not None:
            witnesses["coherence"] = _witness_payload(
                "coherence",
                coherence_gap,
                thresholds["coherence_gap"],
                "<=",
                extra={"summary": f"Coherence gap {coherence_gap:.4f} ≤ {thresholds['coherence_gap']:.4f}"},
            )
        else:
            coherence_score = _coerce_float(metrics_block.get("coherence_score"))
            if coherence_score is None and self.coherence_typed is not None:
                coherence_score = _coerce_float(getattr(self.coherence_typed, "delta", None))
            if coherence_score is not None:
                witnesses["coherence"] = _witness_payload(
                    "coherence",
                    coherence_score,
                    thresholds["coherence_score"],
                    ">=",
                    extra={"summary": f"Coherence score {coherence_score:.3f} ≥ {thresholds['coherence_score']:.3f}"},
                )

        monotonicity_rho = _coerce_float(metrics_block.get("monotonicity_rho"))
        monotonicity_threshold = _coerce_float(
            metrics_block.get("monotonicity_threshold") or metrics_block.get("monotonicity_threshold_value")
        )
        if monotonicity_rho is not None and monotonicity_threshold is not None:
            witnesses.setdefault(
                "monotonicity",
                _witness_payload(
                    "monotonicity",
                    monotonicity_rho,
                    monotonicity_threshold,
                    ">=",
                    extra={
                        "summary": (
                            f"Monotonicity ρ {monotonicity_rho:.3f} ≥ "
                            f"{monotonicity_threshold:.3f}"
                        )
                    },
                ),
            )

        if witnesses:
            metrics_clone = dict(metrics_block)
            metrics_clone["witnesses"] = witnesses
            object.__setattr__(self, "metrics", metrics_clone)
        return witnesses

    @property
    def proof(self) -> ProofOfImpossibility:
        witnesses = self._ensure_witness_block()
        failures: List[Tuple[str, str]] = []
        nearest_hints: List[Dict[str, Any]] = []
        max_gap = 0.0

        for label, payload in witnesses.items():
            comparator = str(payload.get("comparator", ""))
            value = _coerce_float(payload.get("value"))
            threshold = _coerce_float(payload.get("threshold"))
            evaluation = _evaluate_witness(value, threshold, comparator)
            if evaluation is False:
                failures.append(
                    (
                        label,
                        f"value={value} comparator {comparator} threshold={threshold}",
                    )
                )
                nearest_hints.append(
                    {
                        "gate": label,
                        "required": threshold,
                        "comparator": comparator,
                    }
                )
                gap = payload.get("gap")
                if isinstance(gap, (int, float)) and gap > max_gap:
                    max_gap = float(gap)

        return ProofOfImpossibility(
            search_space=[
                {"gate": label, "value": payload.get("value"), "threshold": payload.get("threshold")}
                for label, payload in witnesses.items()
            ],
            failures=failures,
            nearest_valid={"hints": nearest_hints},
            gap=max_gap if max_gap > 0 else 0.0,
            certificate="Gate thresholds violated; witnesses constructed from refusal metrics.",
            metadata={"witnesses": witnesses},
        )

    def verify(self) -> bool:
        witnesses = self._ensure_witness_block()
        if not witnesses:
            return False
        proof = self.proof
        if not isinstance(proof, ProofOfImpossibility):
            return False
        if proof.gap <= 0:
            return False
        # At least one failing witness is required
        has_failure = False
        for payload in witnesses.values():
            comparator = str(payload.get("comparator", ""))
            value = _coerce_float(payload.get("value"))
            threshold = _coerce_float(payload.get("threshold"))
            evaluation = _evaluate_witness(value, threshold, comparator)
            if evaluation is False:
                has_failure = True
            # Ensure gap is coherent when failing
            gap = payload.get("gap")
            if evaluation is False and not (isinstance(gap, (int, float)) and gap >= 0):
                return False
        return has_failure

    @property
    def data(self) -> Dict[str, Any]:
        return dict(self.metrics or {})


class GateWitnessNode(BaseModel):
    """Structured evaluation tree for gate witness proofs."""

    model_config = ConfigDictLike(frozen=True)

    node_type: Literal["all", "any", "predicate", "slice", "slice_instance"]
    label: Optional[str] = None
    expression: Optional[str] = None
    passed: bool
    children: List["GateWitnessNode"] = Field(default_factory=list)
    evidence: Dict[str, Any] = Field(default_factory=dict)


class GateWitnessVerdict(BaseModel):
    """Flattened verdict summary for guard checks with latency metadata."""

    model_config = ConfigDictLike(frozen=True)

    node_type: Literal["all", "any", "predicate", "slice", "slice_instance"]
    path: str = Field(description="Hierarchical path for the evaluated node")
    label: Optional[str] = None
    expression: Optional[str] = None
    passed: bool
    latency_ms: Optional[float] = Field(
        default=None,
        description="Wall clock latency attributed to evaluating this node",
    )


class GateWitness(BaseReceipt):
    """Per-decision transparency record describing the gates that fired."""

    receipt_type: str = "GateWitness"
    decision_receipt_id: str = Field(description="Primary receipt identifier for prophecy/refusal decision output")
    decision_type: Literal["ProphecyStone", "RefusalStone"] = Field(
        description="Decision payload type the witness accompanies",
    )
    decision_allowed: bool = Field(description="Outcome of gate evaluation for the decision")
    refusal_reasons: List[str] = Field(default_factory=list, description="Refusal reasons contributing to the verdict")
    dsl: str = Field(description="Gatebook DSL source evaluated for this witness")
    evaluation: GateWitnessNode = Field(description="Structured verdict tree for evaluated rules")
    metrics_snapshot: Dict[str, Any] = Field(
        default_factory=dict,
        description="Redacted metrics snapshot captured at evaluation time for reproducibility",
    )
    status_snapshot: Dict[str, Any] = Field(
        default_factory=dict,
        description="Gate status payload (coherence, EVHD, fairness, etc.) used for evaluation",
    )
    policy_bundle_hash: str = Field(default=OPA_POLICY_BUNDLE_SHA256)
    policy_version: str = Field(default=OPA_POLICY_VERSION)
    gatebook_hash: Optional[str] = None
    policy_bundle_receipt_id: Optional[str] = None
    gatebook_receipt_id: Optional[str] = None
    overall_latency_ms: Optional[float] = Field(
        default=None,
        description="Measured wall clock time (milliseconds) for evaluating the witness DSL",
    )
    policy_witness: Optional[PolicyWitness] = Field(
        default=None,
        description="Embedded policy witness snapshot for the decision.",
    )
    verdicts: List[GateWitnessVerdict] = Field(
        default_factory=list,
        description="Flattened verdict summaries for observability and regression tracking",
    )


GateWitnessNode.model_rebuild()


class OutcomeReceipt(BaseReceipt):
    """Realized outcome receipt."""

    model_config = ConfigDictLike(populate_by_name=True)

    receipt_type: str = "OutcomeReceipt"
    asset_id: str
    realized_value: float = Field(
        validation_alias=AliasChoices("realized_value", "realized"),
        serialization_alias="realized",
    )
    target_name: str
    parent_prophecy_id: str = Field(
        validation_alias=AliasChoices("parent_prophecy_id", "parent_id"),
        serialization_alias="parent_id",
    )
    clv_bps: Optional[float] = None
    edge_bps: Optional[float] = None
    fraud_confirmed: Optional[bool] = None
    fraud_confirmation_ts: Optional[datetime] = None
    fraud_investigator: Optional[str] = None
    fraud_notes: Optional[str] = None
    training_split: Optional[str] = None
    training_enqueued_at: Optional[datetime] = None
    training_tag: Optional[str] = None
    pa_sla: Optional[PAOutcomeSLA] = None
    residual: Optional[float] = Field(
        default=None,
        description="Realized minus predicted probability when outcome recorded",
    )
    latency_minutes: Optional[float] = Field(
        default=None,
        description="Latency between prophecy timestamp and outcome recording, in minutes",
    )
    cohort: Optional[str] = Field(
        default=None,
        description="Optional cohort identifier used for rolling calibration slices",
    )

    @property
    def realized(self) -> float:
        return self.realized_value

    @property
    def parent_id(self) -> str:
        return self.parent_prophecy_id

    @model_validator(mode="after")
    def _ensure_prophecy_link(self) -> "OutcomeReceipt":
        parent_id = self.parent_prophecy_id
        if parent_id:
            if not self.parent_ids:
                object.__setattr__(self, "parent_ids", [parent_id])
            related = dict(self.related_ids or {})
            related.setdefault("prophecy", parent_id)
            object.__setattr__(self, "related_ids", related)
        return self

    @model_validator(mode="before")
    @classmethod
    def _coerce_legacy_aliases(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        payload = dict(data)
        if "game_id" in payload and "asset_id" not in payload:
            payload["asset_id"] = payload.pop("game_id")
        if "prediction_target" in payload and "target_name" not in payload:
            payload["target_name"] = payload.pop("prediction_target")
        if "prediction_value" in payload and "realized_value" not in payload:
            # outcome receipts use realized_value as canonical; allow prediction_value mapping
            payload.setdefault("realized_value", payload.pop("prediction_value"))
        if "outcome_value" in payload and "realized_value" not in payload:
            payload.setdefault("realized_value", payload.pop("outcome_value"))
        if "prophecy_id" in payload and "parent_prophecy_id" not in payload:
            payload["parent_prophecy_id"] = payload.pop("prophecy_id")
        if "ci_lower" in payload and "ci90_lo" not in payload:
            payload["ci90_lo"] = payload.pop("ci_lower")
        if "ci_upper" in payload and "ci90_hi" not in payload:
            payload["ci90_hi"] = payload.pop("ci_upper")
        return payload


__all__ = [
    "ProphecyStone",
    "RefusalStone",
    "OutcomeReceipt",
    "GateWitness",
    "GateWitnessNode",
    "GateWitnessVerdict",
]
