from __future__ import annotations

import base64
import logging
import re
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Dict, Iterable, List, Literal, Mapping, MutableMapping, Optional, Sequence, Set
from urllib.parse import urlparse

from pydantic import AliasChoices

from receipts.compat.pyd import BaseModel, ConfigDictLike, Field, field_validator, model_serializer, model_validator, strip_signatures
from receipts.canonicalize import to_jcs_bytes, compute_payload_hash
try:
    from loom.proof_config import AttestationLevel
except ImportError:  # Standalone assay-ai install without full CCIO
    from enum import Enum as _Enum

    class AttestationLevel(str, _Enum):  # type: ignore[no-redef]
        """Attestation levels (standalone fallback)."""
        L0_DEV = "L0_DEV"
        L1_SIGNED = "L1_SIGNED"
        L2_TIMESTAMPED = "L2_TIMESTAMPED"
        L3_ANCHORED = "L3_ANCHORED"
        L4_WITNESSED = "L4_WITNESSED"
        L5_COURT_GRADE = "L5_COURT_GRADE"

        def __lt__(self, other: "AttestationLevel") -> bool:
            order = list(AttestationLevel)
            return order.index(self) < order.index(other)

try:  # pragma: no cover - optional dependency during tests
    from engine.gates.core import GateResult as _GateResult  # type: ignore
except Exception:  # pragma: no cover - fallback stub
    from dataclasses import dataclass
    from typing import Generic, List, Optional, TypeVar

    T = TypeVar("T")

    @dataclass
    class _GateResult(Generic[T]):
        """Minimal GateResult for clinical guardian tests."""

        value: Optional[T]
        passed: bool
        reasons: List[str]

        def __bool__(self) -> bool:
            return self.passed

GateResult = _GateResult

try:  # pragma: no cover - pydantic v1 lacks SerializationInfo
    from pydantic import SerializationInfo
except ImportError:  # pragma: no cover - exercised with pydantic v1
    SerializationInfo = Any  # type: ignore[assignment]

try:
    from api.middleware import get_request_id
except ImportError:  # Standalone assay-ai install without Starlette/API layer
    def get_request_id() -> Optional[str]:  # type: ignore[misc]
        return None
from receipts.immutability import freeze_model, immutable_receipts_enabled


logger = logging.getLogger("loom.receipts.base")


class Domain(str, Enum):
    MLB = "mlb"
    MARKETS = "markets"
    GOVERNANCE = "governance"
    CLINICAL = "clinical"
    LEGACY_PA = "legacy_pa"


LegacyDomainAliases: Dict[str, Domain] = {
    "claims": Domain.LEGACY_PA,
    "prior_auth": Domain.LEGACY_PA,
    "energy": Domain.LEGACY_PA,
}


# Domain Sigils (for CLI/docs/Sheets display only - NOT runtime logic)
DOMAIN_SIGILS: Dict[Domain, str] = {
    Domain.MLB: "⚾",
    Domain.CLINICAL: "⚕️",
    Domain.MARKETS: "📊",
    Domain.GOVERNANCE: "⚖️",
    Domain.LEGACY_PA: "📋",
}


class ReceiptSchemaVersion(str, Enum):
    V2_0 = "2.0"
    V2_1 = "2.1"
    V2_2 = "2.2"
    V2_3 = "2.3"
    V3_0 = "3.0"

    @classmethod
    def default(cls) -> "ReceiptSchemaVersion":
        return cls.V3_0


SCHEMA_VERSION = ReceiptSchemaVersion.default().value
OVERRIDE_JUSTIFICATION_DENYLIST = {"override", "test", "na", "loremipsum"}
_LEGACY_SIGNATURE_WARNED: Set[str] = set()


def _resolve_domain_namespace(raw: Any) -> Domain:
    if isinstance(raw, Domain):
        return raw
    if isinstance(raw, str):
        token = raw.strip().lower()
        if not token:
            return Domain.GOVERNANCE
        member = Domain._value2member_map_.get(token)
        if member is not None:
            return member
        alias = LegacyDomainAliases.get(token)
        if alias is not None:
            return alias
    return Domain.GOVERNANCE


def _normalise_utc_millis(dt: datetime) -> datetime:
    """Return a UTC datetime truncated to millisecond precision."""

    if dt.tzinfo is None:
        coerced = dt.replace(tzinfo=timezone.utc)
    else:
        coerced = dt.astimezone(timezone.utc)
    if coerced.microsecond:
        coerced = coerced.replace(microsecond=(coerced.microsecond // 1000) * 1000)
    return coerced


class Ed25519Signature(BaseModel):
    """Typed Ed25519 signature envelope used across receipts."""

    model_config = ConfigDictLike(frozen=True)

    algorithm: str = Field(default="ed25519", description="Signature algorithm identifier")
    kid: str = Field(description="Key identifier for the signing key (JWKS kid).")
    signature_b64: str = Field(description="Base64-encoded Ed25519 signature.")
    signed_at: datetime = Field(description="UTC timestamp when the signature was produced.")

    @model_validator(mode="after")
    def _normalise_timestamp(self) -> "Ed25519Signature":
        object.__setattr__(self, "signed_at", _normalise_utc_millis(self.signed_at))
        return self

    @field_validator("signature_b64")
    @classmethod
    def _validate_signature_b64(cls, value: str) -> str:
        try:
            decoded = base64.b64decode(value, validate=True)
        except Exception as exc:  # pragma: no cover - Python <3.11 ValueError variants
            raise ValueError("signature_b64 must be valid base64") from exc
        if len(decoded) != 64:
            raise ValueError("signature_b64 must decode to 64 bytes for Ed25519")
        return value

    @model_serializer
    def _serialize(self, info: SerializationInfo) -> Dict[str, Any]:
        include = getattr(info, "include", None)
        exclude = getattr(info, "exclude", None)

        def _allowed(name: str) -> bool:
            if include is not None:
                if isinstance(include, Mapping):
                    if name not in include:
                        return False
                elif isinstance(include, (set, frozenset)):
                    if name not in include:
                        return False
            if exclude is not None:
                if isinstance(exclude, Mapping):
                    if name in exclude:
                        return False
                elif isinstance(exclude, (set, frozenset)):
                    if name in exclude:
                        return False
            return True

        payload: Dict[str, Any] = {}
        if _allowed("algorithm"):
            payload["algorithm"] = self.algorithm
        if _allowed("kid"):
            payload["kid"] = self.kid
        if _allowed("signature_b64"):
            payload["signature_b64"] = self.signature_b64
        if _allowed("signed_at"):
            ts = self.signed_at
            if info.mode == "json":
                payload["signed_at"] = ts.isoformat().replace("+00:00", "Z")
            else:
                payload["signed_at"] = ts
        return payload

    def model_dump(self, *args: Any, mode: Optional[str] = None, **kwargs: Any) -> Dict[str, Any]:
        data = super().model_dump(*args, mode=mode, **kwargs)
        include = kwargs.get("include")
        exclude = kwargs.get("exclude")

        def _permitted(name: str) -> bool:
            if include is not None:
                if isinstance(include, Mapping):
                    if name not in include:
                        return False
                elif isinstance(include, (set, frozenset)):
                    if name not in include:
                        return False
            if exclude is not None:
                if isinstance(exclude, Mapping):
                    if name in exclude:
                        return False
                elif isinstance(exclude, (set, frozenset)):
                    if name in exclude:
                        return False
            return True

        if _permitted("algorithm"):
            data.setdefault("algorithm", self.algorithm)
        if _permitted("kid"):
            data.setdefault("kid", self.kid)
        if _permitted("signature_b64"):
            data.setdefault("signature_b64", self.signature_b64)
        if _permitted("signed_at"):
            if mode == "json":
                data["signed_at"] = self.signed_at.isoformat().replace("+00:00", "Z")
            else:
                data["signed_at"] = self.signed_at
        return data


def _coerce_signature_envelope(raw: Any, field_name: str) -> List[Ed25519Signature]:
    """Convert legacy signature payloads into typed ``Ed25519Signature`` objects."""

    if raw is None:
        return []
    if isinstance(raw, Ed25519Signature):
        return [raw]
    if isinstance(raw, (list, tuple, set)):
        items: Iterable[Any] = raw
    else:
        items = [raw]

    converted: List[Ed25519Signature] = []
    legacy_seen = False
    for item in items:
        if isinstance(item, Ed25519Signature):
            converted.append(item)
            continue
        if isinstance(item, Mapping):
            try:
                converted.append(Ed25519Signature.model_validate(item))
            except Exception as exc:  # pragma: no cover - pydantic error will explain context
                raise ValueError(f"invalid signature payload for {field_name}") from exc
            legacy_seen = True
            continue
        raise TypeError(f"{field_name} must contain signature mapping entries")

    if legacy_seen and field_name not in _LEGACY_SIGNATURE_WARNED:
        logger.warning("coerced legacy signature payload for %s", field_name)
        _LEGACY_SIGNATURE_WARNED.add(field_name)

    return converted


class BaseReceipt(BaseModel):
    """Base receipt model with immutability, domain tracking, and cryptographic helpers."""

    model_config = ConfigDictLike(frozen=True, protected_namespaces=(), extra="forbid")

    receipt_id: str
    domain: str
    domain_namespace: Domain = Field(default=Domain.GOVERNANCE, exclude=True)
    request_id: Optional[str] = Field(
        default=None,
        description="Correlation identifier propagated from X-Request-Id header when available",
    )
    tenant: Optional[str] = Field(default=None, description="Tenant identifier for multi-tenant isolation")
    ts: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
    schema_version: ReceiptSchemaVersion = Field(default=ReceiptSchemaVersion.default())
    rsc_version: str = Field(
        default="v1",
        description="Receipt Standard Core version (v1, v2, etc.) - for future schema migrations"
    )
    mode: str = "LIVE"
    context: Dict[str, Any] = Field(default_factory=dict)
    related_ids: Dict[str, Any] = Field(
        default_factory=dict,
        description="Associative links to upstream receipts",
    )

    thread_id: Optional[str] = Field(default=None, description="Case/claim/request ID for chain grouping")

    parent_ids: List[str] = Field(default_factory=list)
    parent_hashes: List[str] = Field(default_factory=list)
    parent_hash: Optional[str] = None
    receipt_hash: Optional[str] = None
    signatures: List[Ed25519Signature] = Field(
        default_factory=list,
        validation_alias=AliasChoices("signatures", "signature"),
        serialization_alias="signatures",
        description="Detached signature envelopes for cryptographic attestation",
    )
    attestation_level: AttestationLevel = Field(
        default=AttestationLevel.L0_DEV,
        description=(
            "Actual attestation level achieved for this receipt. "
            "Set by the writer based on signing/anchoring outcome. "
            "L0_DEV = unsigned (dev/test only), L1_SIGNED = Ed25519 signature, "
            "L2_TIMESTAMPED = signature + TSA, etc."
        ),
    )
    allow_orphan: bool = Field(default=False, description="Allow write without parent (DIP roots only)")

    # RFC 3161 timestamp anchoring (for L2_TIMESTAMPED attestation)
    tsa_token_b64: Optional[str] = Field(
        default=None,
        description=(
            "Base64-encoded RFC 3161 timestamp token from a trusted TSA. "
            "When present, attestation_level should be L2_TIMESTAMPED. "
            "Proves receipt existed at a specific time via third-party authority. "
            "Recommended TSA: Sigstore (https://github.com/sigstore/timestamp-authority)."
        ),
    )
    tsa_authority: Optional[str] = Field(
        default=None,
        description="URL of the TSA that issued the timestamp token.",
    )

    valid_time: datetime = Field(
        default_factory=lambda: datetime.now(timezone.utc),
        description="Real-world effective time for the recorded fact or prediction.",
    )
    system_time: Optional[datetime] = Field(
        default=None,
        description="Timestamp assigned when the receipt is persisted to storage.",
    )

    zk_claims: List[Dict[str, Any]] = Field(default_factory=list, description="Zero-knowledge compliance proofs")

    # CloudEvents 1.0 envelope (optional, for court-grade receipts)
    ce_specversion: Optional[Literal["1.0"]] = Field(
        default=None,
        description="CloudEvents spec version (1.0)"
    )
    ce_id: Optional[str] = Field(
        default=None,
        description="CloudEvents event ID (globally unique)"
    )
    ce_source: Optional[str] = Field(
        default=None,
        description="CloudEvents source URI (e.g., /apm/receipts/payment/{agent_id})"
    )
    ce_type: Optional[str] = Field(
        default=None,
        description="CloudEvents type (e.g., loom.apm.payment)"
    )
    ce_time: Optional[datetime] = Field(
        default=None,
        description="CloudEvents timestamp (RFC 3339)"
    )
    ce_datacontenttype: Optional[str] = Field(
        default="application/json",
        description="CloudEvents data content type"
    )
    traceparent: Optional[str] = Field(
        default=None,
        description="W3C Trace Context traceparent (00-trace_id-span_id-flags)"
    )

    # Constitutional runtime extensions (optional; may be unset for legacy receipts)
    action_class: Optional[str] = Field(
        default=None,
        description="Risk-aware class of action (observe/read/suggest/act/clinical_action/robotic_motion/config_change)",
    )
    learning_tier: Optional[str] = Field(
        default=None,
        description="Derived learning tier for this receipt (none/offline_only/low/high)",
    )
    learning_eligible: Optional[bool] = Field(
        default=None,
        description="Whether this receipt is eligible for learning pipelines",
    )
    override_applied: bool = Field(
        default=False,
        description="True when a human/operator override changed the outcome",
    )
    shadow_disagreement: bool = Field(
        default=False,
        description="True when a shadow/normative check disagreed with the original decision",
    )
    shadow_mode: Optional[str] = Field(
        default=None,
        description="Execution mode: live | dry_run | shadow (if applicable)",
    )
    intent_contract_id: Optional[str] = Field(
        default=None,
        description="Intent contract in force for this Episode",
    )
    intent_mode: Optional[str] = Field(
        default=None,
        description="Intent mode from the contract (e.g., draft_only, shadow_only)",
    )
    intent_aligned: Optional[bool] = Field(
        default=None,
        description="Whether this action aligned with the Episode intent contract",
    )
    trust_profile: Optional[Dict[str, Any]] = Field(
        default=None,
        description="Computed TrustProfile snapshot for this receipt",
    )

    # ============================================================================
    # ALIEN-MIND METADATA (Stigmergic Coordination)
    # ============================================================================
    # Optional fields for stigmergic routing and species fitness tracking.
    # See LOOM_ALIEN_MIND_PRINCIPLES.md for design rationale.

    region_id: Optional[str] = Field(
        default=None,
        description="Species ID that produced this receipt (e.g., species:guardian)"
    )
    region_chain: Optional[str] = Field(
        default=None,
        description="Chain of species used in this decision (e.g., chain:guardian→weaver→actor)"
    )
    pattern_sig: Optional[str] = Field(
        default=None,
        description="Context hash (16 hex chars) for stigmergic trail matching"
    )
    reward: Optional[Dict[str, Any]] = Field(
        default=None,
        description="Outcome metrics for trail reinforcement: {success, dignity, latency_ms, cost, refusal, paradox}"
    )

    @model_validator(mode="before")
    @classmethod
    def _inject_request_id(cls, data: Any) -> Any:
        if not isinstance(data, MutableMapping):
            return data
        payload = dict(data)
        if payload.get("request_id"):
            return payload
        request_id = get_request_id()
        if request_id:
            payload["request_id"] = request_id
        return payload

    @field_validator("ce_time")
    @classmethod
    def _ensure_ce_time_is_aware(cls, v: Optional[datetime]) -> Optional[datetime]:
        """CloudEvents ce_time must be timezone-aware (RFC 3339 with Z or offset)."""
        if v is None:
            return v
        if v.tzinfo is None:
            raise ValueError("ce_time must be timezone-aware (RFC3339 with 'Z' or offset)")
        return v

    @field_validator("ce_source")
    @classmethod
    def _validate_ce_source(cls, v: Optional[str]) -> Optional[str]:
        """CloudEvents ce_source must be a non-empty URI-reference."""
        if v is None:
            return v
        p = urlparse(v)
        # CloudEvents allows URI-reference; accept path-only or full URI, but require non-empty
        if not (p.scheme or p.path):
            raise ValueError("ce_source must be a non-empty URI-reference (CloudEvents 1.0)")
        if len(v) > 2048:
            raise ValueError("ce_source too long (max 2048 chars)")
        return v

    @field_validator("traceparent")
    @classmethod
    def _validate_traceparent(cls, v: Optional[str]) -> Optional[str]:
        """W3C Trace Context traceparent must match format: 00-<32 hex>-<16 hex>-<2 hex>."""
        if v is None:
            return v
        # W3C Trace Context 1.0: version-trace_id-parent_id-trace_flags
        if not re.match(r"^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$", v):
            raise ValueError("traceparent must match W3C Trace Context format (00-<32hex>-<16hex>-<2hex>)")
        return v

    @model_validator(mode="before")
    @classmethod
    def _bind_signatures(cls, data: Any) -> Any:
        if not isinstance(data, MutableMapping):
            return data
        payload = dict(data)
        raw = payload.get("signatures")
        if raw is None and "signature" in payload:
            raw = payload["signature"]
        if raw is not None:
            payload["signatures"] = _coerce_signature_envelope(raw, f"{cls.__name__}.signatures")
            payload.pop("signature", None)
        return payload

    @model_validator(mode="before")
    @classmethod
    def _resolve_namespace(cls, data: Any) -> Any:
        if not isinstance(data, MutableMapping):
            return data
        payload: Dict[str, Any] = dict(data)
        raw_domain = payload.get("domain")

        # If domain not provided in the incoming payload, attempt to read
        # a class-level default (subclass may declare `domain = Field(default=...)`).
        if raw_domain is None:
            class_default = getattr(cls, "domain", None)
            if isinstance(class_default, Domain):
                raw_domain = class_default.value
            elif isinstance(class_default, str) and class_default:
                raw_domain = class_default

        if isinstance(raw_domain, Domain):
            payload["domain"] = raw_domain.value

        # Prefer an explicit domain_namespace in the payload; otherwise use raw_domain
        namespace = payload.get("domain_namespace", raw_domain)

        # If still not resolved, check for a class-level domain_namespace default
        if namespace is None:
            cls_ns_default = getattr(cls, "domain_namespace", None)
            if isinstance(cls_ns_default, Domain):
                namespace = cls_ns_default
            elif isinstance(cls_ns_default, str) and cls_ns_default:
                namespace = cls_ns_default

        payload["domain_namespace"] = _resolve_domain_namespace(namespace)
        return payload

    @field_validator("valid_time")
    @classmethod
    def _ensure_valid_time_timezone(cls, value: datetime) -> datetime:
        if value.tzinfo is None:
            raise ValueError("valid_time must be timezone-aware")
        return value

    @field_validator("system_time")
    @classmethod
    def _ensure_system_time_timezone(cls, value: Optional[datetime]) -> Optional[datetime]:
        if value is None:
            return value
        if value.tzinfo is None:
            raise ValueError("system_time must be timezone-aware")
        return value

    @model_validator(mode="after")
    def _normalise_timestamps(self) -> "BaseReceipt":
        object.__setattr__(self, "ts", _normalise_utc_millis(self.ts))
        object.__setattr__(self, "valid_time", _normalise_utc_millis(self.valid_time))
        system_time = getattr(self, "system_time", None)
        if system_time is not None:
            object.__setattr__(self, "system_time", _normalise_utc_millis(system_time))
        fields_set = getattr(self, "model_fields_set", getattr(self, "__pydantic_fields_set__", set()))
        auto_valid_time = "valid_time" not in fields_set
        object.__setattr__(self, "_auto_valid_time", auto_valid_time)
        return self

    @model_validator(mode="after")
    def _freeze_containers(self) -> "BaseReceipt":
        if immutable_receipts_enabled():
            freeze_model(self)
        return self

    def model_dump(self, *args: Any, mode: Optional[str] = None, **kwargs: Any) -> Dict[str, Any]:
        data = super().model_dump(*args, mode=mode, **kwargs)
        if "signatures" in data:
            signatures = getattr(self, "signatures", None)
            if signatures is None:
                data["signatures"] = []
                return data
            serialised: List[Any] = []
            for entry in signatures:
                if isinstance(entry, Ed25519Signature):
                    serialised.append(entry.model_dump(mode=mode, **kwargs))
                else:
                    serialised.append(entry)
            data["signatures"] = serialised
        return data

    @property
    def signature(self) -> Optional[Ed25519Signature]:
        return self.signatures[0] if self.signatures else None

    @property
    def namespace(self) -> Domain:
        return self.domain_namespace

    @property
    def type(self) -> str:
        return getattr(self, "receipt_type", self.__class__.__name__)

    def payload_bytes(self) -> bytes:
        """
        Compute canonical JCS bytes for this receipt (excluding signatures).

        This is the stable byte representation used for:
        - Computing receipt_hash
        - Creating cryptographic signatures
        - Merkle tree construction
        - Anchoring to transparency logs

        The payload explicitly excludes all signature fields to ensure
        deterministic hashing before and after signing.

        Returns:
            Canonical RFC 8785 JCS bytes
        """
        # Convert to dict, strip signatures, then canonicalize
        data = self.model_dump(mode="json")
        payload_data = strip_signatures(data)
        return to_jcs_bytes(payload_data)

    def compute_hash(self, algorithm: str = "sha256") -> str:
        """
        Compute cryptographic hash of the receipt payload.

        This hash excludes signatures (detached signature pattern) and
        uses JCS canonicalization for determinism.

        Args:
            algorithm: Hash algorithm (sha256 or sha512)

        Returns:
            Hex-encoded hash string
        """
        return compute_payload_hash(self, algorithm=algorithm)


class RecourseItem(BaseModel):
    bundle: str
    audience: Literal["operator", "member", "provider"]
    message: str
    code: Optional[str] = None
    evidence: Optional[str] = None
    policy_anchor: Optional[str] = None
    severity: Optional[str] = None


class CoherenceStatus(BaseModel):
    estimator: str = Field(default="predictive", description="Estimator identifier used for guard evaluation")
    deltaC: Optional[float] = Field(default=None, description="ΔC value produced by the selected estimator")
    passed: bool = Field(default=True, description="Final guard outcome after applying configured floor checks")
    floor: Optional[float] = Field(default=None, description="Configured ΔC floor for the estimator, if any")
    agreement_count: Optional[int] = Field(default=None, description="Triangulation agreement count when available")
    summary_passed: Optional[bool] = Field(default=None, description="Two-of-three coherence summary pass flag")
    insufficient_data: bool = Field(default=False, description="True when guard bypassed due to insufficient support")
    evidence: Dict[str, Any] = Field(default_factory=dict, description="Supporting metrics captured during evaluation")
    estimator_passed: Optional[bool] = Field(default=None, description="Raw estimator outcome before ΔC floors")
    score: Optional[float] = Field(default=None, description="Aggregate coherence score after normalisation")
    failed_estimators: List[str] = Field(default_factory=list, description="Estimator identifiers that failed")


class RefusalBudgetStatus(BaseModel):
    window: Optional[str] = Field(default=None, description="Configured rolling window label, e.g., '7d'")
    lo: Optional[float] = Field(default=None, description="Lower refusal-rate band bound")
    hi: Optional[float] = Field(default=None, description="Upper refusal-rate band bound")
    observed_rate: Optional[float] = Field(default=None, description="Historical refusal rate prior to decision")
    projected_rate: Optional[float] = Field(default=None, description="Projected refusal rate including decision")
    historical_count: int = Field(default=0, description="Number of historical decisions within the window")
    projected_count: int = Field(default=0, description="Projected decision count including decision")
    refusals: int = Field(default=0, description="Projected refusal count including decision")
    state: Optional[str] = Field(default=None, description="Position relative to the band")
    min_samples: int = Field(default=0, description="Minimum sample threshold before enforcing the band")
    sufficient: bool = Field(default=False, description="True when sample size meets minimum threshold")
    auto_tighten_enabled: bool = Field(default=False, description="Allow automatic tightening when above band")
    auto_relax_enabled: bool = Field(default=False, description="Allow automatic relaxation when below band")
    action: Optional[str] = Field(default=None, description="Suggested action (tighten/relax) when enabled")
    passed: Optional[bool] = Field(default=None, description="Guard outcome for this evaluation")
    utilization: Optional[float] = Field(default=None, description="Projected utilisation relative to the upper band")
    mode: Optional[str] = Field(default=None, description="Pipeline mode evaluated for the band")
    override: bool = Field(default=False, description="True when guard failure was bypassed")


class EVHDStatus(BaseModel):
    rho: Optional[float] = None
    n_bins: int = 0
    min_support: int = 0
    support: int = 0
    min_rho: Optional[float] = None
    passed: bool = False


class MonotonicityCheck(BaseModel):
    label: str
    passed: bool
    comparator: Optional[str] = Field(default=None, description="Comparator applied to value vs threshold")
    value: Optional[float] = Field(default=None, description="Observed metric value")
    threshold: Optional[float] = Field(default=None, description="Configured threshold")
    tolerance: Optional[float] = Field(default=None, description="Additional tolerance during evaluation")
    sample_size: Optional[int] = Field(default=None, description="Observed sample size for the check")
    min_sample: Optional[int] = Field(default=None, description="Minimum sample required before enforcement")
    reason: Optional[str] = Field(default=None, description="Structured refusal reason when failed")
    context: Dict[str, Any] = Field(default_factory=dict, description="Additional context captured for the check")


class MonotonicityStatus(BaseModel):
    rho: Optional[float] = None
    threshold: Optional[float] = None
    n_pairs: int = 0
    min_pairs: int = 0
    passed: bool = False
    coherence_gap: Optional[float] = Field(default=None, description="Observed margin vs direct coherence gap")
    checks: Dict[str, MonotonicityCheck] = Field(default_factory=dict)


class GateStatus(BaseModel):
    ece_worst: Optional[float] = None
    coverage_90: Optional[float] = None
    evhd_spearman: Optional[float] = None
    evhd_n_bins: Optional[int] = None
    min_per_decile: Optional[int] = None
    evhd_support: Optional[int] = None
    evhd: Optional[EVHDStatus] = None
    coherence: Optional[CoherenceStatus] = None
    refusal_budget: Optional[RefusalBudgetStatus] = None
    ensemble: Optional[Dict[str, Any]] = None
    monotonicity: Optional[MonotonicityStatus] = None
    passed: bool = False


GateStatus.model_rebuild()


class PADenialReasonPayload(BaseModel):
    code: str
    category: str
    operator_text: str
    member_text: str
    provider_text: str
    ui_string: str
    policy_anchor_template: str
    evidence_slot: str
    severity: str


class ReceiptAnchor(BaseModel):
    """
    Cryptographic anchor binding a receipt to external proof systems.
    
    Stores complete proof bundles for offline verification:
    - TSA (RFC 3161): Timestamp authority proof of existence
    - Rekor: Sigstore transparency log inclusion proof
    - Sigstore bundle: Full certificate chain + inclusion proof + checkpoint
    
    Court-grade anchors require both TSA and Rekor proofs.
    """
    model_config = ConfigDictLike(frozen=True)

    # Core hash (JCS-canonicalized, signature-stripped)
    payload_sha256: str = Field(
        description="Canonical SHA-256 digest of the receipt body (excluding anchor/signature metadata).",
    )
    anchored_at: datetime = Field(
        default_factory=lambda: datetime.now(timezone.utc),
        description="UTC timestamp when the anchor was minted.",
    )
    anchor_version: str = Field(
        default="v2",
        description="Anchor schema version identifier.",
    )
    
    # -------------------------------------------------------------------------
    # TSA (RFC 3161) Timestamp
    # -------------------------------------------------------------------------
    tsa_token_b64: Optional[str] = Field(
        default=None,
        description="Base64-encoded RFC 3161 timestamp token (DER format).",
    )
    tsa_timestamp: Optional[datetime] = Field(
        default=None,
        description="Claimed timestamp from TSA response.",
    )
    tsa_serial: Optional[str] = Field(
        default=None,
        description="TSA certificate serial number for audit trail.",
    )
    tsa_url: Optional[str] = Field(
        default=None,
        description="TSA server URL used for timestamping.",
    )
    tsa_hash_algorithm: Optional[str] = Field(
        default="sha256",
        description="Hash algorithm used in TSA request.",
    )
    
    # -------------------------------------------------------------------------
    # Rekor Transparency Log
    # -------------------------------------------------------------------------
    rekor_uuid: Optional[str] = Field(
        default=None,
        description="Sigstore Rekor transparency log entry UUID.",
    )
    rekor_log_index: Optional[int] = Field(
        default=None,
        ge=0,
        description="Rekor log index associated with the anchor entry.",
    )
    rekor_integrated_time: Optional[int] = Field(
        default=None,
        ge=0,
        description="Rekor integrated time (Unix epoch seconds).",
    )
    rekor_inclusion_proof: Optional[Dict[str, Any]] = Field(
        default=None,
        description="Full Rekor inclusion proof payload returned by the transparency log.",
    )
    rekor_log_url: Optional[str] = Field(
        default=None,
        description="Rekor log URL used (e.g., https://rekor.sigstore.dev).",
    )
    
    # -------------------------------------------------------------------------
    # Full Sigstore Bundle (for offline verification)
    # -------------------------------------------------------------------------
    sigstore_bundle: Optional[Dict[str, Any]] = Field(
        default=None,
        description="Complete Sigstore bundle: {signature_b64, cert_chain_b64, inclusion_proof, checkpoint}.",
    )
    
    # -------------------------------------------------------------------------
    # Proof Status
    # -------------------------------------------------------------------------
    is_mock: bool = Field(
        default=False,
        description="True if proofs are mock/test (not court-grade).",
    )
    
    @property
    def has_tsa(self) -> bool:
        """True if TSA timestamp is present."""
        return self.tsa_token_b64 is not None
    
    @property
    def has_rekor(self) -> bool:
        """True if Rekor entry is present."""
        return self.rekor_uuid is not None or self.rekor_log_index is not None
    
    @property
    def is_court_grade(self) -> bool:
        """True if anchor has real (non-mock) TSA and Rekor proofs."""
        return self.has_tsa and self.has_rekor and not self.is_mock


class PolicyPin(BaseModel):
    """
    Exact policy snapshot for replay determinism.
    
    Every receipt should include a PolicyPin to enable exact replay:
    - policy_id: Human-readable policy identifier
    - git_commit: Exact commit SHA of the policy repository
    - policy_version: Semantic version or digest
    - gatebook_digest: SHA-256 of the gatebook YAML at decision time
    
    Without exact policy pinning, replay is meaningless.
    """
    model_config = ConfigDictLike(frozen=True)
    
    policy_id: str = Field(
        description="Human-readable policy identifier (e.g., 'guardian.clinical.v2').",
    )
    git_commit: str = Field(
        description="Git commit SHA of the policy repository at decision time.",
    )
    policy_version: Optional[str] = Field(
        default=None,
        description="Semantic version or digest of the policy bundle.",
    )
    gatebook_digest: Optional[str] = Field(
        default=None,
        description="SHA-256 digest of the gatebook YAML configuration.",
    )
    policy_repo: Optional[str] = Field(
        default=None,
        description="Repository URL for policy source (e.g., 'github.com/org/policies').",
    )
    effective_at: Optional[datetime] = Field(
        default=None,
        description="Timestamp when this policy version became effective.",
    )
    
    @classmethod
    def from_current(cls, policy_id: str, git_commit: Optional[str] = None) -> "PolicyPin":
        """
        Create a PolicyPin from the current environment.
        
        Args:
            policy_id: Policy identifier
            git_commit: Git commit (auto-detected if not provided)
            
        Returns:
            PolicyPin with current values
        """
        import subprocess
        
        if git_commit is None:
            try:
                result = subprocess.run(
                    ["git", "rev-parse", "HEAD"],
                    capture_output=True,
                    text=True,
                    timeout=5,
                )
                if result.returncode == 0:
                    git_commit = result.stdout.strip()
            except Exception:
                pass
        
        return cls(
            policy_id=policy_id,
            git_commit=git_commit or "unknown",
        )


class PolicyWitness(BaseModel):
    gatebook_digest: str = Field(description="SHA-256 digest of the gatebook policy applied")
    bundle_sha256: Optional[str] = Field(
        default=None,
        description="SHA-256 digest of the compiled policy bundle",
    )
    bundle_version: Optional[str] = Field(
        default=None,
        description="Semantic version of the policy bundle",
    )
    verdict: Optional[str] = Field(
        default=None,
        description="Outcome verdict token recorded by the gate evaluation (ALLOW/REFUSE/WARN/BYPASS).",
    )
    latency_budget_ms: Optional[int] = Field(
        default=None,
        ge=0,
        description="Static latency budget enforced for the domain",
    )
    latency_measured_ms: Optional[int] = Field(
        default=None,
        ge=0,
        validation_alias=AliasChoices("latency_ms", "latency_measured_ms"),
        description="Measured deliberation latency in milliseconds",
    )
    evaluator_version: Optional[str] = Field(default=None, description="Evaluator implementation version")
    policy_id: Optional[str] = Field(default=None, description="Identifier for the evaluated policy")
    policy_version: Optional[str] = Field(default=None, description="Policy configuration version/digest")
    policy_bundle: Optional[str] = Field(
        default=None,
        description="Human-readable identifier for the Guardian policy bundle.",
    )
    bundle_revision: Optional[str] = Field(
        default=None,
        description="Revision identifier sourced from the Guardian bundle manifest.",
    )
    policy_digest: Optional[str] = Field(
        default=None,
        description="SHA-256 digest recorded in the Guardian decision envelope.",
    )
    schema_version: Optional[str] = Field(
        default=None,
        description="Decision envelope schema version emitted by Guardian.",
    )
    decision_id: Optional[str] = Field(
        default=None,
        description="Stable Guardian decision identifier carried by the envelope.",
    )
    traceparent: Optional[str] = Field(
        default=None,
        description="Trace context identifier associated with the Guardian decision.",
    )
    warn_codes: List[str] = Field(
        default_factory=list,
        description="Guardian warning codes captured from the decision envelope.",
    )
    refusal_taxonomy: List[str] = Field(
        default_factory=list,
        description="Guardian refusal taxonomy codes captured from the envelope.",
    )
    decision_flags: Dict[str, Any] = Field(
        default_factory=dict,
        description="Structured Guardian decision flags (duplicate_detected, prod_creds, etc.).",
    )
    decision_envelope: Optional[Dict[str, Any]] = Field(
        default=None,
        description="Raw Guardian decision envelope payload for full-fidelity audits.",
    )
    inputs_hash: Optional[str] = Field(default=None, description="Canonical SHA-256 digest of policy inputs")
    parent_ids: List[str] = Field(default_factory=list, description="Identifiers of receipts referenced by this witness")
    context: Dict[str, Any] = Field(default_factory=dict, description="Additional structured context for the witness")

    @model_validator(mode="after")
    def _validate_latency(self) -> "PolicyWitness":
        if (
            self.latency_measured_ms is not None
            and self.latency_budget_ms is not None
            and self.latency_measured_ms > self.latency_budget_ms
        ):
            raise ValueError("policy_witness latency budget exceeded")
        return self

    @model_serializer(mode="wrap")
    def _serialise_latency(self, handler: Any) -> Dict[str, Any]:
        payload = handler(self)
        if "latency_ms" not in payload:
            payload["latency_ms"] = self.latency_measured_ms
        return payload


class UncertaintyNeighbor(BaseModel):
    receipt_id: str
    similarity: float
    prediction: float
    refusal: Optional[float] = None
    descriptors: Dict[str, Any] = Field(default_factory=dict)


class UncertaintyLineage(BaseModel):
    window: str
    neighbors: List[UncertaintyNeighbor] = Field(default_factory=list)


class ConformalMetadata(BaseModel):
    alpha: float
    conf_lo: float
    conf_hi: float
    width: float


class GateEvent(BaseModel):
    gate_id: str
    context: Dict[str, Any] = Field(default_factory=dict)


class RefusalMetadata(BaseModel):
    label: str
    reason_code: str
    reason_family: str
    severity: Optional[str] = None
    policy_anchor: Optional[str] = None


class RefusalAuditFix(BaseModel):
    label: str
    applied: bool
    note: Optional[str] = None


class RefusalReasonSummary(BaseModel):
    reason_code: str
    support: int
    rate: float
    cohort: Optional[str] = None


class RefusalLeverRecommendation(BaseModel):
    lever: str
    action: Literal["tighten", "relax", "monitor"]
    rationale: str


class SavedEVTrendPoint(BaseModel):
    window_start: datetime
    window_end: datetime
    saved_ev: float


class GatebookExperimentCandidate(BaseModel):
    receipt_id: str
    run_id: str
    description: str
    metrics: Dict[str, Any] = Field(default_factory=dict)


class PASLADetail(BaseModel):
    label_code: str
    support: int
    refusal_rate: float


class PASLAStatus(BaseModel):
    sla_id: str
    status: Literal["pass", "warn", "fail"]
    details: List[PASLADetail] = Field(default_factory=list)


class PAOutcomeSLA(BaseModel):
    outcome_id: str
    status: Literal["pass", "warn", "fail"]
    support: int
    target_rate: float
    observed_rate: float


class FeatureDriftContributor(BaseModel):
    feature: str
    psi: float
    importance: Optional[float] = None


class ComplianceAssertionProofStep(BaseModel):
    position: Literal["left", "right"] = Field(description="Sibling position relative to the current leaf.")
    hash_b64: str = Field(description="Base64-encoded sibling hash used for Merkle inclusion verification.")


class ComplianceAssertionProof(BaseModel):
    receipt_id: str = Field(description="Receipt identifier tied to the Merkle leaf.")
    leaf_hash_b64: str = Field(description="Base64-encoded SHA-256 hash of the leaf payload.")
    siblings: List[ComplianceAssertionProofStep] = Field(
        default_factory=list,
        description="Ordered list of sibling hashes needed to reconstruct the Merkle root.",
    )


__all__ = [
    "BaseReceipt",
    "Domain",
    "LegacyDomainAliases",
    "SCHEMA_VERSION",
    "OVERRIDE_JUSTIFICATION_DENYLIST",
    "Ed25519Signature",
    "_coerce_signature_envelope",
    "RecourseItem",
    "CoherenceStatus",
    "RefusalBudgetStatus",
    "GateStatus",
    "GateResult",
    "EVHDStatus",
    "MonotonicityCheck",
    "MonotonicityStatus",
    "PADenialReasonPayload",
    "ReceiptAnchor",
    "PolicyPin",
    "PolicyWitness",
    "UncertaintyNeighbor",
    "UncertaintyLineage",
    "ConformalMetadata",
    "GateEvent",
    "RefusalMetadata",
    "RefusalAuditFix",
    "RefusalReasonSummary",
    "RefusalLeverRecommendation",
    "SavedEVTrendPoint",
    "GatebookExperimentCandidate",
    "PASLADetail",
    "PASLAStatus",
    "PAOutcomeSLA",
    "FeatureDriftContributor",
    "ComplianceAssertionProofStep",
    "ComplianceAssertionProof",
]
