"""
ReceiptV1: Canonical Chain-of-Trust Receipt Schema

This module defines the next-generation receipt format with:
- Modular proof claims (not hard-coded tsa_token/rekor_uuid fields)
- Proof profiles as explicit contracts over proof kinds
- Explicit degraded state tracking (no silent downgrades)
- Clean separation: envelope / profile / proofs / proof_status / domain_payload

The canonical region (what gets hashed/signed) is:
  { version, envelope, profile_snapshot, domain_payload }

Proofs and proof_status are NOT part of the canonical region, allowing
proofs to be added/appended later without mutating the signed content.

---
feature:
  id: feature.receipt_v1
  name: ReceiptV1 Chain-of-Trust Schema
  kind: module
  summary: Modular, profile-driven receipt format with explicit proof contracts
  owners: ["@agent-c"]
  stage: production
  tags: [receipts, chain-of-trust, court-grade, p0]
  depends_on: [feature.proof_config, feature.receipt_writer]
  invariants:
    - Canonical region never changes once receipt is created
    - All proofs reference the same content-digest
    - Profiles are embedded at emit time for audit independence
---
"""

from __future__ import annotations

import hashlib
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Dict, List, Literal, Optional, Union

from receipts.compat.pyd import BaseModel, ConfigDictLike, Field, field_validator, model_validator
from loom.proof_config import AttestationLevel


__all__ = [
    "RECEIPT_V1_VERSION",
    "Domain",
    "GuardianVerdict",
    "ProofKind",
    "EnvelopeV1",
    "ProfileSnapshotV1",
    "ContentDigestProof",
    "LocalSignatureProof",
    "TsaRfc3161Proof",
    "TransparencyLogProof",
    "EnclaveAttestationProof",
    "ProofClaimV1",
    "MissingProof",
    "ProofStatusV1",
    "DomainPayloadV1",
    "ReceiptV1",
    "compute_canonical_digest",
]


RECEIPT_V1_VERSION = "receipt.v1"


class Domain(str, Enum):
    """Supported domains for receipts."""
    CLINICAL = "clinical"
    FINANCE = "finance"
    OPS = "ops"
    SANDBOX = "sandbox"
    GOVERNANCE = "governance"
    MARKETS = "markets"
    MLB = "mlb"


class GuardianVerdict(str, Enum):
    """Guardian constitutional review verdict."""
    PASS = "pass"
    SOFT_FAIL = "soft_fail"
    HARD_FAIL = "hard_fail"


class ProofKind(str, Enum):
    """Supported proof claim types."""
    CONTENT_DIGEST = "content-digest"
    LOCAL_SIGNATURE = "local-signature"
    TSA_RFC3161 = "tsa-rfc3161"
    TRANSPARENCY_LOG = "transparency-log"
    ENCLAVE_ATTESTATION = "enclave-attestation"
    PQ_SIGNATURE = "pq-signature"  # Future: post-quantum
    WITNESS_SIGNATURE = "witness-signature"  # Multi-party


# -----------------------------------------------------------------------------
# Envelope: Core invariants (always present, all domains)
# -----------------------------------------------------------------------------

class EnvelopeV1(BaseModel):
    """
    Core envelope containing routing, temporal, and governance fields.

    These fields are part of the canonical region and are hashed/signed.
    """
    model_config = ConfigDictLike(extra="forbid", frozen=True)

    # Identity
    receipt_id: str = Field(..., description="Unique receipt identifier (e.g., rcpt_01HZYZ...)")
    tenant_id: str = Field(..., description="Tenant/organization identifier")
    system_id: str = Field(default="loom-core", description="Emitting system identifier")
    domain: Domain = Field(..., description="Domain namespace (clinical, finance, ops, etc.)")
    jurisdiction: Optional[str] = Field(default=None, description="Jurisdiction code (e.g., US-CA)")
    app_id: Optional[str] = Field(default=None, description="Emitting application identifier")

    # Tri-temporal timestamps
    event_time: datetime = Field(..., description="E - When the triggering event occurred")
    observed_time: datetime = Field(..., description="O - When Loom witnessed the event")
    receipt_time: datetime = Field(..., description="R - When receipt was committed")

    # Attestation
    attestation_level: str = Field(..., description="Achieved attestation level (L0_DEV..L5_COURT_GRADE)")
    proof_profile_id: str = Field(..., description="Human-readable profile name (e.g., clinical-grade)")

    # Trust spine
    previous_receipt_hash: Optional[str] = Field(default=None, description="Hash of previous receipt in chain")

    # Governance
    guardian_verdict: GuardianVerdict = Field(..., description="Constitutional review result")
    dignity_score: float = Field(..., ge=0.0, le=1.0, description="Dignity score (D >= min for profile)")
    coherence_delta: float = Field(..., description="Coherence delta from decision")

    @field_validator("attestation_level")
    @classmethod
    def validate_attestation_level(cls, v: str) -> str:
        """Ensure attestation level is valid."""
        valid = [level.value for level in AttestationLevel]
        if v not in valid:
            raise ValueError(f"Invalid attestation_level: {v}. Must be one of {valid}")
        return v


# -----------------------------------------------------------------------------
# Profile Snapshot: Proof contract at emit time
# -----------------------------------------------------------------------------

class ProfileSnapshotV1(BaseModel):
    """
    Frozen snapshot of the proof profile used at emit time.

    Embedded in each receipt so audits never depend on external config.
    """
    model_config = ConfigDictLike(extra="forbid", frozen=True)

    proof_profile_id: str = Field(..., description="Profile name (e.g., clinical-grade)")
    attestation_level: str = Field(..., description="Target attestation level for this profile")

    # Proof contract
    required_proofs: List[str] = Field(..., description="Proof kinds that MUST be present")
    optional_proofs: List[str] = Field(default_factory=list, description="Proof kinds that MAY be present")

    # Constraints
    min_dignity: float = Field(default=0.0, ge=0.0, le=1.0, description="Minimum dignity score")
    enforce_tri_temporal: bool = Field(default=True, description="Enforce E <= O <= R ordering")
    extra_witness_required: Optional[str] = Field(default=None, description="e.g., human_clinician")

    @field_validator("required_proofs", "optional_proofs")
    @classmethod
    def validate_proof_kinds(cls, v: List[str]) -> List[str]:
        """Validate proof kinds are known."""
        valid = {k.value for k in ProofKind}
        for kind in v:
            if kind not in valid:
                raise ValueError(f"Unknown proof kind: {kind}. Must be one of {valid}")
        return v


# -----------------------------------------------------------------------------
# Proof Claims: Modular, extensible proof types
# -----------------------------------------------------------------------------

class ContentDigestProof(BaseModel):
    """SHA-256 hash of the canonical region."""
    model_config = ConfigDictLike(extra="forbid", frozen=True)

    kind: Literal["content-digest"] = "content-digest"
    alg: Literal["sha-256"] = "sha-256"
    value: str = Field(..., description="Base64-encoded digest bytes")


class LocalSignatureProof(BaseModel):
    """Ed25519 signature over the content digest."""
    model_config = ConfigDictLike(extra="forbid", frozen=True)

    kind: Literal["local-signature"] = "local-signature"
    alg: Literal["ed25519"] = "ed25519"
    kid: str = Field(..., description="Key identifier for rotation tracking")
    sig: str = Field(..., description="Base64-encoded signature bytes")


class TsaRfc3161Proof(BaseModel):
    """RFC 3161 Time Stamp Authority token."""
    model_config = ConfigDictLike(extra="forbid", frozen=True)

    kind: Literal["tsa-rfc3161"] = "tsa-rfc3161"
    tsa_id: str = Field(..., description="TSA identifier (e.g., digicert-prod-01)")
    token: str = Field(..., description="Base64-encoded RFC 3161 timestamp token")
    tsa_time: datetime = Field(..., description="Timestamp from TSA")


class TransparencyLogProof(BaseModel):
    """Sigstore Rekor transparency log entry."""
    model_config = ConfigDictLike(extra="forbid", frozen=True)

    kind: Literal["transparency-log"] = "transparency-log"
    log: Literal["rekor"] = "rekor"
    entry_uuid: str = Field(..., description="Rekor entry UUID")
    log_index: int = Field(..., description="Rekor log index")
    integrated_time: datetime = Field(..., description="When Rekor integrated the entry")
    epoch_id: str = Field(..., description="Epoch identifier for batch anchoring")
    merkle_path: List[str] = Field(..., description="Merkle audit path for this leaf")


class EnclaveAttestationProof(BaseModel):
    """TEE/enclave attestation (future)."""
    model_config = ConfigDictLike(extra="forbid", frozen=True)

    kind: Literal["enclave-attestation"] = "enclave-attestation"
    enclave_type: str = Field(..., description="e.g., sgx, sev, nitro")
    attestation: str = Field(..., description="Base64-encoded attestation blob")
    measurement: str = Field(..., description="Enclave measurement/PCR")


# Union of all proof types
ProofClaimV1 = Union[
    ContentDigestProof,
    LocalSignatureProof,
    TsaRfc3161Proof,
    TransparencyLogProof,
    EnclaveAttestationProof,
]


# -----------------------------------------------------------------------------
# Proof Status: Contract vs reality (no silent downgrades)
# -----------------------------------------------------------------------------

class MissingProof(BaseModel):
    """Record of a required proof that could not be obtained."""
    model_config = ConfigDictLike(extra="forbid", frozen=True)

    kind: str = Field(..., description="The missing proof kind")
    reason: str = Field(..., description="Why it's missing (e.g., REKOR_UNAVAILABLE, TSA_TIMEOUT)")
    degraded_mode: bool = Field(default=True, description="True if this is a degraded state")
    recorded_at: datetime = Field(..., description="When the failure was recorded")


class ProofStatusV1(BaseModel):
    """
    Explicit status of proof contract fulfillment.

    For strict profiles (court-grade, clinical-grade), missing MUST be empty.
    For softer profiles, missing proofs are allowed but marked as degraded.
    """
    model_config = ConfigDictLike(extra="forbid", frozen=True)

    required: List[str] = Field(..., description="Proof kinds required by profile")
    present: List[str] = Field(..., description="Proof kinds actually present")
    missing: List[MissingProof] = Field(default_factory=list, description="Required proofs that failed")

    tri_temporal_ok: bool = Field(default=True, description="E <= O <= R ordering satisfied")
    dignity_ok: bool = Field(default=True, description="dignity_score >= min_dignity")

    @property
    def contract_satisfied(self) -> bool:
        """True if all required proofs are present and constraints met."""
        return (
            len(self.missing) == 0
            and self.tri_temporal_ok
            and self.dignity_ok
            and set(self.required).issubset(set(self.present))
        )

    @property
    def is_degraded(self) -> bool:
        """True if any proofs are missing (degraded mode)."""
        return len(self.missing) > 0


# -----------------------------------------------------------------------------
# Domain Payload: Multi-domain, hot-swappable
# -----------------------------------------------------------------------------

class DomainPayloadV1(BaseModel):
    """
    Domain-specific payload with schema versioning.

    The envelope + proofs stay identical across domains;
    only domain_payload changes.
    """
    model_config = ConfigDictLike(extra="allow", frozen=True)  # Allow domain-specific fields

    kind: str = Field(..., description="Payload type (e.g., prior_authorization_decision)")
    schema_version: str = Field(..., description="Schema version (e.g., clinical.pa.v1)")
    data: Dict[str, Any] = Field(default_factory=dict, description="Domain-specific data")


# -----------------------------------------------------------------------------
# ReceiptV1: Top-level canonical receipt
# -----------------------------------------------------------------------------

class ReceiptV1(BaseModel):
    """
    Canonical ReceiptV1 with modular proofs and explicit proof contracts.

    Structure:
        version         - Schema version ("receipt.v1")
        envelope        - Core invariants (ids, times, guardian, dignity)
        profile_snapshot - Proof profile at emit time (frozen contract)
        proofs          - List of modular proof claims
        proof_status    - Contract vs reality (required/present/missing)
        domain_payload  - Domain-specific data

    Canonical region (what gets hashed/signed):
        { version, envelope, profile_snapshot, domain_payload }

    Proofs and proof_status are NOT hashed, allowing proofs to be
    added/appended later without mutating the signed content.
    """
    model_config = ConfigDictLike(extra="forbid", frozen=True)

    version: Literal["receipt.v1"] = RECEIPT_V1_VERSION
    envelope: EnvelopeV1
    profile_snapshot: ProfileSnapshotV1
    proofs: List[ProofClaimV1] = Field(default_factory=list)
    proof_status: ProofStatusV1
    domain_payload: DomainPayloadV1

    def get_proof(self, kind: str) -> Optional[ProofClaimV1]:
        """Get a proof by kind, or None if not present."""
        for proof in self.proofs:
            if proof.kind == kind:
                return proof
        return None

    def has_proof(self, kind: str) -> bool:
        """Check if a proof kind is present."""
        return self.get_proof(kind) is not None

    @property
    def content_digest(self) -> Optional[str]:
        """Get the content digest if present."""
        proof = self.get_proof("content-digest")
        if proof and isinstance(proof, ContentDigestProof):
            return proof.value
        return None

    @property
    def is_valid(self) -> bool:
        """Quick check: is the proof contract satisfied?"""
        return self.proof_status.contract_satisfied

    def to_canonical_region(self) -> Dict[str, Any]:
        """
        Extract the canonical region for hashing/signing.

        This is the ONLY part that gets hashed. Proofs and proof_status
        are excluded so they can be added later.
        """
        return {
            "version": self.version,
            "envelope": self.envelope.model_dump(mode="json"),
            "profile_snapshot": self.profile_snapshot.model_dump(mode="json"),
            "domain_payload": self.domain_payload.model_dump(mode="json"),
        }


# -----------------------------------------------------------------------------
# Utility: Compute canonical digest
# -----------------------------------------------------------------------------

def compute_canonical_digest(receipt: ReceiptV1) -> str:
    """
    Compute SHA-256 digest of the canonical region.

    Returns base64-encoded digest string.
    """
    import base64
    from jcs import canonicalize as jcs_canonicalize

    canonical_region = receipt.to_canonical_region()
    jcs_bytes = jcs_canonicalize(canonical_region)
    digest = hashlib.sha256(jcs_bytes).digest()
    return base64.b64encode(digest).decode("ascii")
