"""
ProofArtifact — The universal output contract for every DAG step.

Every step in a PG-DAG produces exactly one ProofArtifact.  This is the
only thing downstream nodes trust.  Mutable state is a read-cache;
proofs are the source of truth.

Idempotency key = inputs_fingerprint + step_id + spec_version.
If the gate has seen this key before, the prior proof is returned
and execution is short-circuited.

Merkle root is a binary-tree hash over all proof hashes in a run,
computed at slot closure.  This is the durable record of "this run
happened, in this order, with these results."
"""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Dict, List


# ---------------------------------------------------------------------------
# Proof status
# ---------------------------------------------------------------------------

class ProofStatus(str, Enum):
    """Terminal status for a step's proof artifact."""
    COMPLETED = "completed"
    FAILED = "failed"
    INTERRUPTED = "interrupted"


# ---------------------------------------------------------------------------
# ProofArtifact
# ---------------------------------------------------------------------------

@dataclass(frozen=True, slots=True)
class ProofArtifact:
    """Universal output of every PG-DAG step.

    Frozen so it cannot be mutated after creation.  Downstream nodes
    receive proof hashes, not mutable objects.

    Attributes:
        step_id:              Stable step name (e.g. "seal_risk_envelope").
        spec_version:         Envelope schema version (bumped on cDNA mutation).
        inputs_fingerprint:   SnapChore hash of canonicalized inputs.
        outputs_fingerprint:  SnapChore hash of canonicalized outputs.
        proof_hash:           SnapChore seal of the step envelope.
        block_ref:            SmartBlock id/hash that publishes the step.
        status:               Terminal status (completed | failed | interrupted).
        metrics:              Domain-specific metrics (yield, trust, entropy, etc.).
        created_at:           When the proof was sealed.
    """
    step_id: str
    spec_version: str
    inputs_fingerprint: str
    outputs_fingerprint: str
    proof_hash: str
    block_ref: str
    status: ProofStatus
    metrics: Dict[str, Any] = field(default_factory=dict)
    created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    @property
    def idempotency_key(self) -> str:
        """Content-addressed key for duplicate detection."""
        return f"{self.inputs_fingerprint}:{self.step_id}:{self.spec_version}"

    @property
    def succeeded(self) -> bool:
        return self.status == ProofStatus.COMPLETED

    def to_dict(self) -> Dict[str, Any]:
        return {
            "step_id": self.step_id,
            "spec_version": self.spec_version,
            "inputs_fingerprint": self.inputs_fingerprint,
            "outputs_fingerprint": self.outputs_fingerprint,
            "proof_hash": self.proof_hash,
            "block_ref": self.block_ref,
            "status": self.status.value,
            "metrics": self.metrics,
            "created_at": self.created_at.isoformat(),
        }


# ---------------------------------------------------------------------------
# Merkle root
# ---------------------------------------------------------------------------

def merkle_root(hashes: List[str]) -> str:
    """Compute a binary Merkle root over a list of hex-encoded hashes.

    Used at slot closure to produce a single root covering all step
    proof hashes in the run.

    - Empty list  → empty string
    - Single hash → that hash
    - Odd count   → last hash is duplicated (standard padding)
    """
    if not hashes:
        return ""
    if len(hashes) == 1:
        return hashes[0]

    layer = list(hashes)
    while len(layer) > 1:
        if len(layer) % 2 == 1:
            layer.append(layer[-1])  # duplicate last for odd count
        next_layer: List[str] = []
        for i in range(0, len(layer), 2):
            combined = layer[i] + layer[i + 1]
            h = hashlib.sha256(combined.encode()).hexdigest()
            next_layer.append(h)
        layer = next_layer
    return layer[0]


# ---------------------------------------------------------------------------
# Canonical fingerprinting (fallback when SnapChore unavailable)
# ---------------------------------------------------------------------------

def canonical_fingerprint(data: Dict[str, Any]) -> str:
    """SHA-256 over deterministically serialized dict.

    This is the local fallback when SnapChore capture is not available.
    SnapChore uses the same canonical JSON approach server-side.
    """
    raw = json.dumps(data, sort_keys=True, default=str, separators=(",", ":"))
    return hashlib.sha256(raw.encode()).hexdigest()
