"""
Self-Verifying Receipts - Receipts that carry proof of their own validity.

Every receipt includes witnesses (gate evaluation evidence) that can be
deterministically re-evaluated to verify the receipt's claims.

Design Philosophy:
- Receipts are self-contained proof objects
- Witnesses reference evidence by hash (privacy + size safety)
- verify() deterministically re-evaluates against current gatebook
- Proof obligations enforced by type system

Proof Obligations:
- Prophecy: MUST include Dignity, Calibration, EVHD, Coherence witnesses
- Refusal: MUST include Proof-of-Impossibility witness
- Outcome: MUST link to parent Prophecy via receipt_id
"""

from __future__ import annotations

import hashlib
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Protocol, Tuple

from receipts.jcs import canonicalize


@dataclass
class GateWitness:
    """
    Witness record for a single gate evaluation.
    
    Attributes:
        name: Gate identifier (e.g., "dignity_floor")
        passed: Whether gate passed
        evidence_hash: BLAKE2b hash of canonical (JCS) evidence
        summary: Human-readable one-liner
        evidence: Optional full evidence (not included in receipt hash)
        evaluated_at: When gate was evaluated
    """
    name: str
    passed: bool
    evidence_hash: str
    summary: str
    evidence: Optional[Dict[str, Any]] = None
    evaluated_at: Optional[datetime] = field(default_factory=lambda: datetime.now(timezone.utc))
    
    @classmethod
    def from_evidence(
        cls,
        name: str,
        passed: bool,
        evidence: Dict[str, Any],
        summary: str,
    ) -> GateWitness:
        """Create witness from evidence with canonical hashing."""
        canonical_bytes = canonicalize(evidence)
        evidence_hash = hashlib.blake2b(canonical_bytes, digest_size=32).hexdigest()
        
        return cls(
            name=name,
            passed=passed,
            evidence_hash=evidence_hash,
            summary=summary,
            evidence=evidence,
        )
    
    def to_dict(self, include_evidence: bool = False) -> Dict[str, Any]:
        """
        Export witness to dict.
        
        Args:
            include_evidence: Whether to include full evidence blob
            
        Returns:
            Witness dict (evidence omitted unless requested)
        """
        result = {
            "name": self.name,
            "passed": self.passed,
            "evidence_hash": self.evidence_hash,
            "summary": self.summary,
            "evaluated_at": self.evaluated_at.isoformat() if self.evaluated_at else None,
        }
        
        if include_evidence and self.evidence is not None:
            result["evidence"] = self.evidence
        
        return result


class Gatebook(Protocol):
    """
    Protocol for gatebook (gate registry).
    
    A gatebook is a registry of gate expressions that can evaluate
    receipts to produce (passed, evidence) tuples.
    """
    
    def evaluate(self, receipt: SelfVerifyingReceipt) -> Dict[str, bool]:
        """
        Evaluate receipt against current gates.
        
        Args:
            receipt: Receipt to evaluate
            
        Returns:
            Dict mapping gate names to pass/fail
        """
        ...


class SelfVerifyingReceipt(ABC):
    """
    Base class for receipts that can verify their own witnesses.
    
    All consequential receipts (Prophecy, Refusal, Outcome) inherit from this.
    
    Attributes:
        receipt_id: Unique receipt identifier
        witnesses: List of gate evaluation witnesses
        domain: Receipt domain (stocks, mlb, ncaab, etc.)
        created_at: Receipt creation timestamp
    """
    
    def __init__(
        self,
        receipt_id: str,
        witnesses: List[GateWitness],
        domain: str,
        created_at: Optional[datetime] = None,
    ):
        self.receipt_id = receipt_id
        self.witnesses = witnesses
        self.domain = domain
        self.created_at = created_at or datetime.now(timezone.utc)
    
    @abstractmethod
    def required_witnesses(self) -> List[str]:
        """
        Return list of required witness names for this receipt type.
        
        Enforces proof obligations:
        - Prophecy: ["dignity_floor", "calibration_ece", "evhd_monotonicity", "coherence"]
        - Refusal: ["proof_of_impossibility"]
        """
        pass
    
    def verify(self, gatebook: Gatebook) -> bool:
        """
        Deterministically verify receipt witnesses against gatebook.
        
        Re-evaluates gates and checks that results match witness claims.
        
        Args:
            gatebook: Current gatebook to evaluate against
            
        Returns:
            True if all witnesses match current gate evaluation
        """
        # Check proof obligations
        required = set(self.required_witnesses())
        present = {w.name for w in self.witnesses}
        
        if not required.issubset(present):
            missing = required - present
            return False  # Missing required witnesses
        
        # Re-evaluate gates
        recomputed = gatebook.evaluate(self)
        
        # Check witness claims match recomputation
        for witness in self.witnesses:
            if witness.name not in recomputed:
                continue  # Gate no longer in gatebook (soft failure)
            
            if witness.passed != recomputed[witness.name]:
                return False  # Witness claim doesn't match
        
        return True
    
    def witness_summary(self) -> Dict[str, Any]:
        """
        Generate summary of witness status.
        
        Returns:
            Dict with pass/fail counts and failed gate names
        """
        passed_count = sum(1 for w in self.witnesses if w.passed)
        failed_count = len(self.witnesses) - passed_count
        failed_gates = [w.name for w in self.witnesses if not w.passed]
        
        return {
            "total": len(self.witnesses),
            "passed": passed_count,
            "failed": failed_count,
            "failed_gates": failed_gates,
        }
    
    def to_dict(self, include_evidence: bool = False) -> Dict[str, Any]:
        """
        Export receipt to dict.
        
        Args:
            include_evidence: Whether to include full evidence blobs
            
        Returns:
            Receipt dict suitable for storage
        """
        return {
            "receipt_id": self.receipt_id,
            "domain": self.domain,
            "created_at": self.created_at.isoformat(),
            "witnesses": [w.to_dict(include_evidence=include_evidence) for w in self.witnesses],
        }


@dataclass
class ProphecyReceipt(SelfVerifyingReceipt):
    """
    Self-verifying prophecy (prediction) receipt.
    
    Proof obligations:
    - dignity_floor: DWF ≥ 0.70
    - calibration_ece: ECE ≤ 0.03, |coverage - 0.90| ≤ 0.02
    - evhd_monotonicity: Realized edge increases with predicted edge
    - coherence: |ML_margin - ML_direct| ≤ 0.01
    """
    
    probability: float
    ci90_low: float
    ci90_high: float
    market_prior: Optional[float] = None
    blended_probability: Optional[float] = None
    
    def __post_init__(self):
        # Ensure parent fields exist
        if not hasattr(self, 'receipt_id'):
            self.receipt_id = ""
        if not hasattr(self, 'domain'):
            self.domain = ""
        if not hasattr(self, 'witnesses'):
            self.witnesses = []
        if not hasattr(self, 'created_at'):
            self.created_at = datetime.now(timezone.utc)
    
    def required_witnesses(self) -> List[str]:
        witnesses = [
            "dignity_floor",
            "calibration_ece",
            "evhd_monotonicity",
        ]
        
        # Coherence required if market prior available
        if self.market_prior is not None:
            witnesses.append("coherence")
        
        return witnesses
    
    def to_dict(self, include_evidence: bool = False) -> Dict[str, Any]:
        base = super().to_dict(include_evidence=include_evidence)
        base.update({
            "probability": self.probability,
            "ci90_low": self.ci90_low,
            "ci90_high": self.ci90_high,
            "market_prior": self.market_prior,
            "blended_probability": self.blended_probability,
        })
        return base


@dataclass
class RefusalReceipt(SelfVerifyingReceipt):
    """
    Self-verifying refusal receipt.
    
    Proof obligations:
    - proof_of_impossibility: Evidence of why no prophecy could satisfy gates
    """
    
    reasons: List[str] = field(default_factory=list)
    failed_gates: List[str] = field(default_factory=list)
    
    def __post_init__(self):
        # Ensure parent fields exist
        if not hasattr(self, 'receipt_id'):
            self.receipt_id = ""
        if not hasattr(self, 'domain'):
            self.domain = ""
        if not hasattr(self, 'witnesses'):
            self.witnesses = []
        if not hasattr(self, 'created_at'):
            self.created_at = datetime.now(timezone.utc)
    
    def required_witnesses(self) -> List[str]:
        return ["proof_of_impossibility"]
    
    def to_dict(self, include_evidence: bool = False) -> Dict[str, Any]:
        base = super().to_dict(include_evidence=include_evidence)
        base.update({
            "reasons": self.reasons,
            "failed_gates": self.failed_gates,
        })
        return base


@dataclass
class OutcomeReceiptSV(SelfVerifyingReceipt):
    """
    Self-verifying outcome receipt.
    
    Links to parent Prophecy and records realized outcome.
    """
    
    parent_prophecy_id: str
    outcome: bool  # True = event occurred, False = did not occur
    settled_at: Optional[datetime] = None
    
    def __post_init__(self):
        # Ensure parent fields exist
        if not hasattr(self, 'receipt_id'):
            self.receipt_id = ""
        if not hasattr(self, 'domain'):
            self.domain = ""
        if not hasattr(self, 'witnesses'):
            self.witnesses = []
        if not hasattr(self, 'created_at'):
            self.created_at = datetime.now(timezone.utc)
        if self.settled_at is None:
            self.settled_at = datetime.now(timezone.utc)
    
    def required_witnesses(self) -> List[str]:
        return []  # Outcomes don't require gate witnesses (facts not predictions)
    
    def to_dict(self, include_evidence: bool = False) -> Dict[str, Any]:
        base = super().to_dict(include_evidence=include_evidence)
        base.update({
            "parent_prophecy_id": self.parent_prophecy_id,
            "outcome": self.outcome,
            "settled_at": self.settled_at.isoformat() if self.settled_at else None,
        })
        return base


# Evidence store interface

class EvidenceStore(Protocol):
    """
    Protocol for evidence blob storage.
    
    Evidence is stored out-of-line from receipts, referenced by hash.
    This keeps receipts compact and privacy-safe (PII redacted before hashing).
    """
    
    def store(self, evidence_hash: str, evidence: Dict[str, Any]) -> None:
        """Store evidence blob by hash."""
        ...
    
    def retrieve(self, evidence_hash: str) -> Optional[Dict[str, Any]]:
        """Retrieve evidence blob by hash."""
        ...


class FileSystemEvidenceStore:
    """
    Simple filesystem-based evidence store.
    
    Stores evidence as {hash}.json files in a directory.
    """
    
    def __init__(self, base_path: Path):
        self.base_path = Path(base_path)
        self.base_path.mkdir(parents=True, exist_ok=True)
    
    def store(self, evidence_hash: str, evidence: Dict[str, Any]) -> None:
        """Store evidence blob."""
        import json
        
        evidence_file = self.base_path / f"{evidence_hash}.json"
        evidence_file.write_text(json.dumps(evidence, indent=2))
    
    def retrieve(self, evidence_hash: str) -> Optional[Dict[str, Any]]:
        """Retrieve evidence blob."""
        import json
        
        evidence_file = self.base_path / f"{evidence_hash}.json"
        
        if not evidence_file.exists():
            return None
        
        return json.loads(evidence_file.read_text())


# Utility functions

def verify_receipt_batch(
    receipts: List[SelfVerifyingReceipt],
    gatebook: Gatebook,
) -> Tuple[int, int, List[str]]:
    """
    Batch verify receipts.
    
    Args:
        receipts: List of receipts to verify
        gatebook: Gatebook to verify against
        
    Returns:
        (passed_count, failed_count, failed_receipt_ids)
    """
    failed_ids = []
    
    for receipt in receipts:
        if not receipt.verify(gatebook):
            failed_ids.append(receipt.receipt_id)
    
    passed = len(receipts) - len(failed_ids)
    failed = len(failed_ids)
    
    return passed, failed, failed_ids


def check_proof_obligations(receipt: SelfVerifyingReceipt) -> Tuple[bool, List[str]]:
    """
    Check if receipt satisfies proof obligations.
    
    Args:
        receipt: Receipt to check
        
    Returns:
        (satisfied, missing_witnesses)
    """
    required = set(receipt.required_witnesses())
    present = {w.name for w in receipt.witnesses}
    missing = list(required - present)
    
    return len(missing) == 0, missing
