"""
cDNA — Product-agnostic evolutionary lineage tracking for agent engines.

Every engine agent carries a cDNA string — a compact identity encoding its
current strategy configuration.  When an engine underperforms under specific
entropy conditions, the cDNA MUTATES:

    yield < threshold AND entropy > threshold → evolve

Each mutation is sealed as a proof artifact and appended to a per-engine
lineage chain (chain:cdna:{engine_id}).  The before/after cDNA hashes,
entropy trigger, yield score, and reasoning form an immutable audit trail.

This module is format-agnostic — the SDK does NOT dictate how products
structure their cDNA strings.  Products define their own format; the SDK
tracks lineage, applies mutations via a pluggable mutator, seals proofs,
and manages evidence chains.

Default cDNA generation scheme (products may override):
    gen0: "engine:yield"                    (seed from registration)
    gen1: "engine:yield:g1:a3f2"            (first mutation, hash suffix)
    gen2: "engine:yield:g2:7b01"            (second mutation)

The hash suffix is the first 4 hex chars of SHA-256(previous_cdna + trigger).

Components:
  - CdnaRecord, MutationTrigger, MutationProof — data structures
  - DriftDetector — configurable multi-dimension behavioral drift analysis
  - DriftReport   — result of drift detection
  - EvolutionaryMutator — threshold-gated mutation engine
  - LineageManager — orchestrates the full lifecycle (seed → mutate → seal)

Usage:
    from pgdag.cdna import LineageManager, EvolutionaryMutator

    lineage = LineageManager(
        chain_manager=evidence_chain_manager,
        writer=proof_writer,
        on_mutation=my_reregistration_callback,
    )

    record = lineage.seed("engine-42", "engine:yield")
    proof  = lineage.check_and_mutate(
        "engine-42", yield_score=0.3, entropy_score=0.78,
    )
"""

from __future__ import annotations

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

from .artifact import ProofArtifact, ProofStatus, canonical_fingerprint
from .chains import EvidenceChainManager, chain_id

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------

class MutationStrategy(str, Enum):
    """Why a cDNA mutation was triggered."""
    ENTROPY_YIELD = "entropy_yield"             # Low yield + high entropy
    DRIFT_CORRECTION = "drift_correction"       # Detected behavioral drift
    REGIME_ADAPTATION = "regime_adaptation"      # External regime change
    MANUAL = "manual"                           # Operator-initiated


# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------

@dataclass
class CdnaRecord:
    """A snapshot of an engine's cDNA at a specific generation."""
    engine_id: str
    cdna: str
    generation: int
    spec_version: str
    created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    def to_dict(self) -> Dict[str, Any]:
        return {
            "engine_id": self.engine_id,
            "cdna": self.cdna,
            "generation": self.generation,
            "spec_version": self.spec_version,
            "created_at": self.created_at.isoformat(),
        }


@dataclass(frozen=True)
class MutationTrigger:
    """Captures why a mutation was triggered — frozen for proof immutability."""
    strategy: MutationStrategy
    yield_score: float
    entropy_score: float
    yield_threshold: float = 0.5
    entropy_threshold: float = 0.65
    regime_level: str = ""
    drift_magnitude: float = 0.0
    reasoning: str = ""

    def to_dict(self) -> Dict[str, Any]:
        return {
            "strategy": self.strategy.value,
            "yield_score": self.yield_score,
            "entropy_score": self.entropy_score,
            "yield_threshold": self.yield_threshold,
            "entropy_threshold": self.entropy_threshold,
            "regime_level": self.regime_level,
            "drift_magnitude": self.drift_magnitude,
            "reasoning": self.reasoning,
        }


@dataclass
class MutationProof:
    """Sealed proof of a cDNA mutation event.

    This is appended to the engine's lineage chain as a SmartBlock.
    """
    engine_id: str
    before_cdna: str
    after_cdna: str
    before_generation: int
    after_generation: int
    before_spec_version: str
    after_spec_version: str
    trigger: MutationTrigger
    proof_hash: str = ""
    block_ref: str = ""
    sealed_at: Optional[datetime] = None

    def seal_payload(self) -> Dict[str, Any]:
        """Produce the dict that gets sealed by ProofWriter."""
        return {
            "kind": "cdna_mutation",
            "engine_id": self.engine_id,
            "before": self.before_cdna,
            "after": self.after_cdna,
            "generation": self.after_generation,
            "spec_version": self.after_spec_version,
            "trigger": self.trigger.to_dict(),
            "sealed_at": (self.sealed_at or datetime.now(timezone.utc)).isoformat(),
        }

    def to_dict(self) -> Dict[str, Any]:
        d = self.seal_payload()
        d["before_generation"] = self.before_generation
        d["before_spec_version"] = self.before_spec_version
        d["proof_hash"] = self.proof_hash
        d["block_ref"] = self.block_ref
        return d


@dataclass
class DriftReport:
    """Result of drift detection across multiple dimensions."""
    engine_id: str
    drifted: bool
    magnitude: float                              # 0.0 – 1.0
    dimensions: Dict[str, float] = field(default_factory=dict)
    recommendation: str = ""                      # "no_action" | "monitor" | "mutate"
    reasoning: str = ""
    detected_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    def to_dict(self) -> Dict[str, Any]:
        return {
            "engine_id": self.engine_id,
            "drifted": self.drifted,
            "magnitude": self.magnitude,
            "dimensions": self.dimensions,
            "recommendation": self.recommendation,
            "reasoning": self.reasoning,
            "detected_at": self.detected_at.isoformat(),
        }


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _compute_cdna_hash(previous_cdna: str, trigger_data: str) -> str:
    """SHA-256(previous_cdna:trigger_data) → first 4 hex chars."""
    raw = f"{previous_cdna}:{trigger_data}"
    return hashlib.sha256(raw.encode()).hexdigest()[:4]


def _next_spec_version(current: str, major: bool = False) -> str:
    """Bump a dotted spec version string.

    "0.1.0" → "0.2.0" (minor)   or "1.0.0" (major)
    """
    parts = current.split(".")
    while len(parts) < 3:
        parts.append("0")
    try:
        nums = [int(p) for p in parts[:3]]
    except ValueError:
        nums = [0, 1, 0]

    if major:
        nums[0] += 1
        nums[1] = 0
        nums[2] = 0
    else:
        nums[1] += 1
        nums[2] = 0

    return ".".join(str(n) for n in nums)


# ---------------------------------------------------------------------------
# DriftDetector
# ---------------------------------------------------------------------------

class DriftDetector:
    """Multi-dimensional drift detection for engine behavior.

    Monitors up to 4 dimensions (all optional — products skip what they
    don't track):
      - template_distribution: shift in which templates the engine runs
      - success_rate: drop in step success/failure ratio
      - yield_trajectory: trend in yield over recent runs
      - timing: change in average execution duration

    Usage:
        detector = DriftDetector(template_drift_threshold=0.30)
        report   = detector.detect(
            "engine-42",
            current_template_dist={"yield_capture": 5, "rebalance": 10},
            baseline_template_dist={"yield_capture": 10, "rebalance": 5},
            recent_success_rate=0.72,
            baseline_success_rate=0.95,
        )
    """

    def __init__(
        self,
        *,
        template_drift_threshold: float = 0.25,
        success_rate_floor: float = 0.80,
        yield_drift_threshold: float = 0.15,
        timing_drift_factor: float = 2.0,
    ) -> None:
        self._template_threshold = template_drift_threshold
        self._success_floor = success_rate_floor
        self._yield_threshold = yield_drift_threshold
        self._timing_factor = timing_drift_factor

    def detect(
        self,
        engine_id: str,
        *,
        current_template_dist: Optional[Dict[str, int]] = None,
        baseline_template_dist: Optional[Dict[str, int]] = None,
        recent_success_rate: float = 1.0,
        baseline_success_rate: float = 1.0,
        recent_yield: float = 0.0,
        baseline_yield: float = 0.0,
        recent_avg_duration_ms: float = 0,
        baseline_avg_duration_ms: float = 0,
    ) -> DriftReport:
        """Detect drift by comparing current behaviour against baseline.

        Gracefully handles missing dimensions — only checks dimensions
        where both current and baseline data are provided.
        """
        dimensions: Dict[str, float] = {}
        reasons: List[str] = []

        # 1. Template distribution drift (Jensen-Shannon style)
        if current_template_dist and baseline_template_dist:
            td = self._template_drift(current_template_dist, baseline_template_dist)
            dimensions["template_distribution"] = round(td, 4)
            if td > self._template_threshold:
                reasons.append(
                    f"template distribution drifted {td:.2%} "
                    f"(threshold {self._template_threshold:.0%})"
                )

        # 2. Success rate drop
        if baseline_success_rate > 0:
            sr_delta = baseline_success_rate - recent_success_rate
            dimensions["success_rate"] = round(sr_delta, 4)
            if recent_success_rate < self._success_floor:
                reasons.append(
                    f"success rate {recent_success_rate:.1%} below "
                    f"floor {self._success_floor:.1%}"
                )

        # 3. Yield trajectory
        if baseline_yield > 0:
            y_delta = abs(baseline_yield - recent_yield) / baseline_yield
            dimensions["yield_trajectory"] = round(y_delta, 4)
            if y_delta > self._yield_threshold:
                reasons.append(
                    f"yield drifted {y_delta:.2%} "
                    f"(threshold {self._yield_threshold:.0%})"
                )

        # 4. Timing drift
        if baseline_avg_duration_ms > 0 and recent_avg_duration_ms > 0:
            t_ratio = recent_avg_duration_ms / baseline_avg_duration_ms
            dimensions["timing"] = round(t_ratio, 4)
            if t_ratio > self._timing_factor:
                reasons.append(
                    f"execution time {t_ratio:.1f}x baseline "
                    f"(threshold {self._timing_factor:.1f}x)"
                )

        # Overall magnitude = max dimension drift (normalised 0-1)
        magnitude = 0.0
        if dimensions:
            normalised = []
            if "template_distribution" in dimensions:
                normalised.append(dimensions["template_distribution"])
            if "success_rate" in dimensions:
                normalised.append(min(1.0, dimensions["success_rate"]))
            if "yield_trajectory" in dimensions:
                normalised.append(min(1.0, dimensions["yield_trajectory"]))
            if "timing" in dimensions:
                normalised.append(min(1.0, (dimensions["timing"] - 1.0) / self._timing_factor))
            magnitude = max(normalised) if normalised else 0.0

        drifted = len(reasons) >= 2 or magnitude > 0.5
        recommendation = "mutate" if drifted else ("monitor" if reasons else "no_action")

        return DriftReport(
            engine_id=engine_id,
            drifted=drifted,
            magnitude=round(magnitude, 4),
            dimensions=dimensions,
            recommendation=recommendation,
            reasoning="; ".join(reasons) if reasons else "no significant drift",
        )

    @staticmethod
    def _template_drift(
        current: Dict[str, int],
        baseline: Dict[str, int],
    ) -> float:
        """Jensen-Shannon-style divergence between template distributions."""
        all_keys = set(current) | set(baseline)
        if not all_keys:
            return 0.0

        total_c = sum(current.values()) or 1
        total_b = sum(baseline.values()) or 1

        divergence = 0.0
        for k in all_keys:
            p = current.get(k, 0) / total_c
            q = baseline.get(k, 0) / total_b
            m = (p + q) / 2
            if m > 0:
                if p > 0:
                    divergence += 0.5 * p * (p / m)
                if q > 0:
                    divergence += 0.5 * q * (q / m)
        # Normalise to 0-1 range (JS divergence is 0-ln2 for binary)
        return min(1.0, divergence)


# ---------------------------------------------------------------------------
# EvolutionaryMutator
# ---------------------------------------------------------------------------

class EvolutionaryMutator:
    """Threshold-gated cDNA mutation engine.

    Core mutation condition:
        yield < yield_threshold  AND  entropy > entropy_threshold

    The mutate() method applies the default generation scheme:
        base_name:g{N}:{hash4}

    Products that use a different cDNA format can subclass and override
    mutate() while reusing should_mutate() and the spec-version logic.

    Usage:
        mutator = EvolutionaryMutator(yield_threshold=0.5, entropy_threshold=0.65)
        if mutator.should_mutate(yield_score=0.3, entropy_score=0.78):
            new_record, proof = mutator.mutate(current_record, trigger)
    """

    def __init__(
        self,
        *,
        yield_threshold: float = 0.5,
        entropy_threshold: float = 0.65,
        drift_detector: Optional[DriftDetector] = None,
    ) -> None:
        self._yield_threshold = yield_threshold
        self._entropy_threshold = entropy_threshold
        self._drift_detector = drift_detector or DriftDetector()

    @property
    def yield_threshold(self) -> float:
        return self._yield_threshold

    @property
    def entropy_threshold(self) -> float:
        return self._entropy_threshold

    @property
    def drift_detector(self) -> DriftDetector:
        return self._drift_detector

    def should_mutate(self, yield_score: float, entropy_score: float) -> bool:
        """Check the core mutation condition.

        High entropy (chaotic/unpredictable) + low yield (poor performance)
        = the system's evolutionary response to poor conditions.
        """
        return yield_score < self._yield_threshold and entropy_score > self._entropy_threshold

    def mutate(
        self,
        current: CdnaRecord,
        trigger: MutationTrigger,
    ) -> tuple[CdnaRecord, MutationProof]:
        """Apply a mutation and produce a new record + proof.

        Default generation scheme:
            "engine:yield"          → "engine:yield:g1:a3f2"
            "engine:yield:g1:a3f2"  → "engine:yield:g2:7b01"

        Returns (new_CdnaRecord, MutationProof).
        """
        new_gen = current.generation + 1
        trigger_data = json.dumps(trigger.to_dict(), sort_keys=True)
        hash_suffix = _compute_cdna_hash(current.cdna, trigger_data)

        # Extract base name: first two colon-separated segments
        base_parts = current.cdna.split(":")
        base_name = ":".join(base_parts[:2])  # e.g. "engine:yield"
        new_cdna = f"{base_name}:g{new_gen}:{hash_suffix}"

        # Bump spec version (major for regime/manual, minor otherwise)
        is_major = trigger.strategy in (
            MutationStrategy.REGIME_ADAPTATION,
            MutationStrategy.MANUAL,
        )
        new_spec = _next_spec_version(current.spec_version, major=is_major)

        new_record = CdnaRecord(
            engine_id=current.engine_id,
            cdna=new_cdna,
            generation=new_gen,
            spec_version=new_spec,
        )

        proof = MutationProof(
            engine_id=current.engine_id,
            before_cdna=current.cdna,
            after_cdna=new_cdna,
            before_generation=current.generation,
            after_generation=new_gen,
            before_spec_version=current.spec_version,
            after_spec_version=new_spec,
            trigger=trigger,
        )

        logger.info(
            "cDNA mutation: %s gen%d→gen%d cdna=%s→%s spec=%s→%s reason=%s",
            current.engine_id, current.generation, new_gen,
            current.cdna, new_cdna,
            current.spec_version, new_spec,
            trigger.strategy.value,
        )

        return new_record, proof


# ---------------------------------------------------------------------------
# Callback type alias
# ---------------------------------------------------------------------------

OnMutationCallback = Callable[[CdnaRecord, MutationProof], Awaitable[None]]


# ---------------------------------------------------------------------------
# LineageManager
# ---------------------------------------------------------------------------

class LineageManager:
    """Manages the evolutionary lineage of all engine agents.

    Tracks current cDNA per engine, applies mutations, seals proofs,
    and appends to evidence chains.

    Products wire an ``on_mutation`` callback for their specific needs:
      - Stillpoint: re-register agent with SBN, refresh JWT
      - Grid: notify frontier manager of engine adaptation
      - Dominion: update worker configuration
      - Sonic: adjust settlement rail preferences

    Evidence chains: chain:cdna:{engine_id}

    Usage:
        lineage = LineageManager(
            chain_manager=evidence_chain_manager,
            writer=proof_writer,
            on_mutation=my_reregister_fn,
        )

        record = lineage.seed("sp-engine-yield", "engine:yield")
        proof  = lineage.check_and_mutate(
            "sp-engine-yield",
            yield_score=0.3,
            entropy_score=0.78,
        )
    """

    def __init__(
        self,
        mutator: Optional[EvolutionaryMutator] = None,
        *,
        chain_manager: Any = None,
        writer: Any = None,
        on_mutation: Optional[OnMutationCallback] = None,
    ) -> None:
        self._mutator = mutator or EvolutionaryMutator()
        self._chains = chain_manager
        self._writer = writer
        self._on_mutation = on_mutation

        # Per-engine state
        self._records: Dict[str, CdnaRecord] = {}
        self._history: Dict[str, List[MutationProof]] = {}

    @property
    def mutator(self) -> EvolutionaryMutator:
        return self._mutator

    # ------------------------------------------------------------------
    # Seed
    # ------------------------------------------------------------------

    def seed(
        self,
        engine_id: str,
        cdna: str,
        spec_version: str = "0.1.0",
    ) -> CdnaRecord:
        """Seed initial cDNA for an engine (called at registration time)."""
        record = CdnaRecord(
            engine_id=engine_id,
            cdna=cdna,
            generation=0,
            spec_version=spec_version,
        )
        self._records[engine_id] = record
        self._history.setdefault(engine_id, [])

        # Ensure lineage chain exists
        if self._chains is not None:
            cid = chain_id("cdna", engine_id)
            self._chains.ensure_chain(cid)

        logger.info("cDNA seeded: %s cdna=%s spec=%s", engine_id, cdna, spec_version)
        return record

    # ------------------------------------------------------------------
    # Accessors
    # ------------------------------------------------------------------

    def get_current(self, engine_id: str) -> Optional[CdnaRecord]:
        """Return the current CdnaRecord for an engine, or None."""
        return self._records.get(engine_id)

    def get_history(self, engine_id: str) -> List[MutationProof]:
        """Return the full mutation history for an engine."""
        return list(self._history.get(engine_id, []))

    # ------------------------------------------------------------------
    # Mutation triggers
    # ------------------------------------------------------------------

    def check_and_mutate(
        self,
        engine_id: str,
        *,
        yield_score: float,
        entropy_score: float,
        regime_level: str = "",
    ) -> Optional[MutationProof]:
        """Post-run mutation check.  Returns MutationProof if mutation occurred."""
        current = self._records.get(engine_id)
        if current is None:
            logger.warning("No cDNA record for %s — seed first", engine_id)
            return None

        if not self._mutator.should_mutate(yield_score, entropy_score):
            return None

        trigger = MutationTrigger(
            strategy=MutationStrategy.ENTROPY_YIELD,
            yield_score=yield_score,
            entropy_score=entropy_score,
            yield_threshold=self._mutator.yield_threshold,
            entropy_threshold=self._mutator.entropy_threshold,
            regime_level=regime_level,
            reasoning=(
                f"yield {yield_score:.4f} < {self._mutator.yield_threshold}, "
                f"entropy {entropy_score:.4f} > {self._mutator.entropy_threshold}"
            ),
        )

        return self._apply_mutation(current, trigger)

    def apply_drift_mutation(
        self,
        engine_id: str,
        drift_report: DriftReport,
        *,
        yield_score: float = 0.0,
        entropy_score: float = 0.0,
    ) -> Optional[MutationProof]:
        """Apply a drift-correction mutation."""
        current = self._records.get(engine_id)
        if current is None:
            return None

        trigger = MutationTrigger(
            strategy=MutationStrategy.DRIFT_CORRECTION,
            yield_score=yield_score,
            entropy_score=entropy_score,
            yield_threshold=self._mutator.yield_threshold,
            entropy_threshold=self._mutator.entropy_threshold,
            drift_magnitude=drift_report.magnitude,
            reasoning=drift_report.reasoning,
        )

        return self._apply_mutation(current, trigger)

    def apply_regime_mutation(
        self,
        engine_id: str,
        *,
        new_regime: str,
        yield_score: float = 0.0,
        entropy_score: float = 0.0,
        reasoning: str = "",
    ) -> Optional[MutationProof]:
        """Apply a regime-adaptation mutation (major version bump)."""
        current = self._records.get(engine_id)
        if current is None:
            return None

        trigger = MutationTrigger(
            strategy=MutationStrategy.REGIME_ADAPTATION,
            yield_score=yield_score,
            entropy_score=entropy_score,
            yield_threshold=self._mutator.yield_threshold,
            entropy_threshold=self._mutator.entropy_threshold,
            regime_level=new_regime,
            reasoning=reasoning or f"regime changed to {new_regime}",
        )

        return self._apply_mutation(current, trigger)

    def apply_manual_mutation(
        self,
        engine_id: str,
        *,
        reasoning: str,
    ) -> Optional[MutationProof]:
        """Operator-initiated manual mutation (major version bump)."""
        current = self._records.get(engine_id)
        if current is None:
            return None

        trigger = MutationTrigger(
            strategy=MutationStrategy.MANUAL,
            yield_score=0.0,
            entropy_score=0.0,
            yield_threshold=self._mutator.yield_threshold,
            entropy_threshold=self._mutator.entropy_threshold,
            reasoning=reasoning,
        )

        return self._apply_mutation(current, trigger)

    # ------------------------------------------------------------------
    # Reporting
    # ------------------------------------------------------------------

    def evolution_timeline(self, engine_id: str) -> Dict[str, Any]:
        """Full auditor view of an engine's evolutionary history."""
        current = self._records.get(engine_id)
        history = self._history.get(engine_id, [])
        cid = chain_id("cdna", engine_id)

        return {
            "engine_id": engine_id,
            "current_cdna": current.cdna if current else None,
            "generation": current.generation if current else 0,
            "spec_version": current.spec_version if current else None,
            "mutation_count": len(history),
            "mutations": [m.to_dict() for m in history],
            "lineage_chain": cid,
        }

    def all_engines_summary(self) -> Dict[str, Any]:
        """Summary of all tracked engines' evolutionary state."""
        engines = []
        for eid, record in self._records.items():
            engines.append({
                **record.to_dict(),
                "mutation_count": len(self._history.get(eid, [])),
                "chain_id": chain_id("cdna", eid),
            })
        return {"engines": engines, "total": len(engines)}

    # ------------------------------------------------------------------
    # Internal mutation machinery
    # ------------------------------------------------------------------

    def _apply_mutation(
        self,
        current: CdnaRecord,
        trigger: MutationTrigger,
    ) -> MutationProof:
        """Apply mutation, seal proof, update state, fire callback."""
        new_record, proof = self._mutator.mutate(current, trigger)

        # Seal
        proof = self._seal_mutation(proof)

        # Update state
        self._records[new_record.engine_id] = new_record
        self._history[new_record.engine_id].append(proof)

        # Append to evidence chain
        if self._chains is not None and proof.proof_hash:
            cid = chain_id("cdna", new_record.engine_id)
            # Build a minimal ProofArtifact for the chain append
            chain_proof = ProofArtifact(
                step_id="cdna_mutation",
                spec_version=new_record.spec_version,
                inputs_fingerprint=canonical_fingerprint(
                    {"before_cdna": current.cdna, "engine_id": current.engine_id}
                ),
                outputs_fingerprint=canonical_fingerprint(proof.seal_payload()),
                proof_hash=proof.proof_hash,
                block_ref=proof.block_ref,
                status=ProofStatus.COMPLETED,
                metrics={"strategy": trigger.strategy.value},
            )
            self._chains.append(
                cid,
                chain_proof,
                run_id=f"mutation-{new_record.engine_id}-gen{new_record.generation}",
                metadata={"trigger": trigger.strategy.value},
            )

        # Fire product callback (async-compatible via fire-and-forget)
        if self._on_mutation is not None:
            import asyncio
            try:
                loop = asyncio.get_running_loop()
                loop.create_task(self._on_mutation(new_record, proof))
            except RuntimeError:
                # No running loop — skip callback (non-async context)
                logger.debug("No event loop for on_mutation callback")

        return proof

    def _seal_mutation(self, proof: MutationProof) -> MutationProof:
        """Seal a MutationProof via ProofWriter if available."""
        payload = proof.seal_payload()
        proof_hash = canonical_fingerprint(payload)
        block_ref = ""

        if self._writer is not None:
            from .envelope import StepEnvelope
            envelope = StepEnvelope(
                step_id="cdna_mutation",
                spec_version=proof.after_spec_version,
                inputs={"before_cdna": proof.before_cdna, "engine_id": proof.engine_id},
                config={},
                inputs_fingerprint=canonical_fingerprint(
                    {"before_cdna": proof.before_cdna, "engine_id": proof.engine_id}
                ),
                outputs=payload,
                outputs_fingerprint=canonical_fingerprint(payload),
            )
            artifact = self._writer.write(
                envelope,
                status=ProofStatus.COMPLETED,
                domain="cdna.evolution",
                metrics={"strategy": proof.trigger.strategy.value},
            )
            proof_hash = artifact.proof_hash
            block_ref = artifact.block_ref

        proof.proof_hash = proof_hash
        proof.block_ref = block_ref
        proof.sealed_at = datetime.now(timezone.utc)
        return proof
