"""
EvidenceChainManager — Inter-run integrity via append-only proof chains.

Slots prove ordering *within* a run (Merkle root).
Chains prove ordering *across* runs (sequential hash-linked append).

Each chain is identified by a chain_id string.  Products define their
own chain types and routing rules.  The kernel provides the append,
verify, and health-check mechanics.

Examples of chain routing (product-specific):
  Stillpoint: chain:treasury:{portfolio_id}, chain:yield:{venue_id}
  Dominion:   chain:payrun:{batch_id}, chain:employee:{worker_id}
  Sonic:      chain:settlement:{rail}, chain:receipt:{merchant_id}
  Grid:       chain:slot:{frontier_id}, chain:bonus:{participant_id}

Chains are non-optional for entities with regulatory reporting obligations.
"""

from __future__ import annotations

import hashlib
import json
import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

from .artifact import ProofArtifact

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Chain data structures
# ---------------------------------------------------------------------------

@dataclass
class ChainEntry:
    """A single entry in an evidence chain."""
    chain_id: str
    sequence: int
    proof_hash: str
    step_id: str
    run_id: str
    entry_hash: str = ""
    prev_hash: str = ""
    metadata: Dict[str, Any] = field(default_factory=dict)
    appended_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    def to_dict(self) -> Dict[str, Any]:
        return {
            "chain_id": self.chain_id,
            "sequence": self.sequence,
            "proof_hash": self.proof_hash,
            "step_id": self.step_id,
            "run_id": self.run_id,
            "entry_hash": self.entry_hash,
            "prev_hash": self.prev_hash,
            "metadata": self.metadata,
            "appended_at": self.appended_at.isoformat(),
        }


@dataclass
class ChainState:
    """Current state of an evidence chain."""
    chain_id: str
    length: int
    head_hash: str
    entries: List[ChainEntry] = field(default_factory=list)
    verified: bool = False
    last_verified_at: Optional[datetime] = None

    def to_dict(self) -> Dict[str, Any]:
        return {
            "chain_id": self.chain_id,
            "length": self.length,
            "head_hash": self.head_hash,
            "verified": self.verified,
            "last_verified_at": self.last_verified_at.isoformat() if self.last_verified_at else None,
        }


@dataclass
class ChainHealthReport:
    """Health check result for an evidence chain."""
    chain_id: str
    length: int
    is_healthy: bool
    break_at_sequence: Optional[int] = None
    break_reason: Optional[str] = None
    verified_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))


# ---------------------------------------------------------------------------
# Chain ID helper
# ---------------------------------------------------------------------------

def chain_id(chain_type: str, key: str) -> str:
    """Build a canonical chain ID.

    Examples:
        chain_id("payrun", "batch-123")   → "chain:payrun:batch-123"
        chain_id("treasury", "port-001")  → "chain:treasury:port-001"
    """
    return f"chain:{chain_type}:{key}"


# ---------------------------------------------------------------------------
# EvidenceChainManager
# ---------------------------------------------------------------------------

class EvidenceChainManager:
    """Manages append-only evidence chains with hash linking.

    Each chain is a sequence of entries where each entry's hash
    includes the previous entry's hash — creating an immutable,
    ordered record.

    Optionally integrates with SnapChore for remote chain persistence.

    Usage:
        chains = EvidenceChainManager(sbn_client=sbn_client)

        # Append a proof to a chain
        entry = chains.append("chain:payrun:batch-42", proof, run_id="run-123")

        # Verify chain integrity
        report = chains.verify("chain:payrun:batch-42")
    """

    def __init__(
        self,
        snapchore: Any = None,
        sbn_client: Any = None,
    ) -> None:
        self._chains: Dict[str, List[ChainEntry]] = {}
        self._snapchore = snapchore
        self._sbn_client = sbn_client

    @staticmethod
    def _hash_entry(entry_data: Dict[str, Any]) -> str:
        """Compute entry hash from entry data + previous hash."""
        raw = json.dumps(entry_data, sort_keys=True, default=str, separators=(",", ":"))
        return hashlib.sha256(raw.encode()).hexdigest()

    def ensure_chain(self, cid: str) -> str:
        """Ensure a chain exists (create if needed). Returns the chain_id."""
        if cid not in self._chains:
            self._chains[cid] = []
            logger.debug("Created evidence chain: %s", cid)

            # Register remotely if SBN available
            if self._snapchore and hasattr(self._snapchore, "create_chain"):
                try:
                    self._snapchore.create_chain(cid)
                except Exception as exc:
                    logger.debug("Remote chain creation failed for %s: %s", cid, exc)
        return cid

    def append(
        self,
        cid: str,
        proof: ProofArtifact,
        run_id: str = "",
        *,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> ChainEntry:
        """Append a proof to an evidence chain."""
        self.ensure_chain(cid)
        chain = self._chains[cid]
        sequence = len(chain)
        prev_hash = chain[-1].entry_hash if chain else ""

        entry_data = {
            "chain_id": cid,
            "sequence": sequence,
            "proof_hash": proof.proof_hash,
            "step_id": proof.step_id,
            "run_id": run_id,
            "prev_hash": prev_hash,
        }
        entry_hash = self._hash_entry(entry_data)

        entry = ChainEntry(
            chain_id=cid,
            sequence=sequence,
            proof_hash=proof.proof_hash,
            step_id=proof.step_id,
            run_id=run_id,
            entry_hash=entry_hash,
            prev_hash=prev_hash,
            metadata=metadata or {},
        )
        chain.append(entry)

        # Persist remotely
        if self._snapchore and hasattr(self._snapchore, "append_to_chain"):
            try:
                self._snapchore.append_to_chain(cid, entry.to_dict())
            except Exception as exc:
                logger.debug("Remote chain append failed for %s: %s", cid, exc)

        return entry

    def get_chain(self, cid: str) -> ChainState:
        """Get the current state of a chain."""
        chain = self._chains.get(cid, [])
        return ChainState(
            chain_id=cid,
            length=len(chain),
            head_hash=chain[-1].entry_hash if chain else "",
            entries=list(chain),
        )

    def chain_ids(self) -> List[str]:
        """List all known chain IDs."""
        return list(self._chains.keys())

    def verify(self, cid: str) -> ChainHealthReport:
        """Verify hash-link integrity of a chain."""
        chain = self._chains.get(cid, [])
        if not chain:
            return ChainHealthReport(
                chain_id=cid, length=0, is_healthy=True,
            )

        for i, entry in enumerate(chain):
            # Check prev_hash linkage
            expected_prev = chain[i - 1].entry_hash if i > 0 else ""
            if entry.prev_hash != expected_prev:
                return ChainHealthReport(
                    chain_id=cid,
                    length=len(chain),
                    is_healthy=False,
                    break_at_sequence=i,
                    break_reason=f"prev_hash mismatch at seq {i}",
                )

            # Re-derive entry hash and check
            entry_data = {
                "chain_id": cid,
                "sequence": entry.sequence,
                "proof_hash": entry.proof_hash,
                "step_id": entry.step_id,
                "run_id": entry.run_id,
                "prev_hash": entry.prev_hash,
            }
            expected_hash = self._hash_entry(entry_data)
            if entry.entry_hash != expected_hash:
                return ChainHealthReport(
                    chain_id=cid,
                    length=len(chain),
                    is_healthy=False,
                    break_at_sequence=i,
                    break_reason=f"entry_hash mismatch at seq {i}",
                )

        return ChainHealthReport(
            chain_id=cid, length=len(chain), is_healthy=True,
        )


# ---------------------------------------------------------------------------
# ChainHealthTracker
# ---------------------------------------------------------------------------

class ChainHealthTracker:
    """Periodic health checker for all registered evidence chains."""

    def __init__(self, manager: EvidenceChainManager) -> None:
        self._manager = manager
        self._registered: List[str] = []

    def register_chain(self, cid: str) -> None:
        """Register a chain for health tracking."""
        if cid not in self._registered:
            self._registered.append(cid)

    def verify_all(self) -> List[ChainHealthReport]:
        """Verify all registered chains and return health reports."""
        return [self._manager.verify(cid) for cid in self._registered]

    def any_broken(self) -> bool:
        """Quick check: are any registered chains unhealthy?"""
        return any(not r.is_healthy for r in self.verify_all())
