"""
cdna_template — Proof-gated DAG template for cDNA mutation lifecycle.

Provides a 5-node linear DAG template that turns the mutation lifecycle
into a fully auditable, proof-gated execution:

  detect_drift → evaluate_trigger → compute_mutation → seal_proof → notify_mutation

Products can use this template with PGDAGRunner for full auditability,
or call LineageManager.check_and_mutate() directly for the procedural path.

Usage:
    from pgdag import build
    from pgdag.cdna import LineageManager
    from pgdag.cdna_template import cdna_mutation_template, cdna_mutation_steps

    layer = build(sbn_client=sbn, domain="cdna.evolution")
    lineage = LineageManager(chain_manager=layer.chains, writer=layer.writer)

    layer.runner.register_steps(cdna_mutation_steps(lineage))
    result = await layer.runner.execute(
        cdna_mutation_template(),
        inputs={
            "engine_id": "engine-42",
            "yield_score": 0.3,
            "entropy_score": 0.78,
            # Optional drift detection inputs:
            "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,
        },
        actor="cdna-lifecycle",
    )
"""

from __future__ import annotations

from typing import Any, Dict

from .dag import DAGEdge, DAGNode, DAGTemplate
from .runner import StepContext


# ---------------------------------------------------------------------------
# Template factory
# ---------------------------------------------------------------------------

def cdna_mutation_template(
    *,
    version: str = "1.0",
    domain: str = "cdna.evolution",
) -> DAGTemplate:
    """Build the cdna_mutation DAG template.

    5-node linear sequence:
      1. detect_drift      — Run DriftDetector, produce DriftReport
      2. evaluate_trigger   — Check mutation conditions, build MutationTrigger
      3. compute_mutation   — Apply EvolutionaryMutator, produce new CdnaRecord
      4. seal_proof         — Seal MutationProof, append to evidence chain
      5. notify_mutation    — Fire on_mutation callback, record result
    """
    return DAGTemplate(
        name="cdna_mutation",
        version=version,
        domain=domain,
        nodes=[
            DAGNode(id="detect_drift",      label="Detect Drift",      step_type="analysis"),
            DAGNode(id="evaluate_trigger",  label="Evaluate Trigger",  step_type="validation"),
            DAGNode(id="compute_mutation",  label="Compute Mutation",  step_type="computation"),
            DAGNode(id="seal_proof",        label="Seal Proof",        step_type="attestation"),
            DAGNode(id="notify_mutation",   label="Notify Mutation",   step_type="notification"),
        ],
        edges=[
            DAGEdge(source="detect_drift",     target="evaluate_trigger"),
            DAGEdge(source="evaluate_trigger",  target="compute_mutation"),
            DAGEdge(source="compute_mutation",  target="seal_proof"),
            DAGEdge(source="seal_proof",        target="notify_mutation"),
        ],
    )


# ---------------------------------------------------------------------------
# Step functions factory
# ---------------------------------------------------------------------------

def cdna_mutation_steps(lineage_manager: Any) -> Dict[str, Any]:
    """Build step functions wired to a LineageManager instance.

    Returns a dict suitable for PGDAGRunner.register_steps().

    Args:
        lineage_manager: A LineageManager instance (from pgdag.cdna).
    """
    from .cdna import (
        DriftReport,
        LineageManager,
        MutationStrategy,
        MutationTrigger,
    )

    lm: LineageManager = lineage_manager

    # ------------------------------------------------------------------
    # Step 1: detect_drift
    # ------------------------------------------------------------------

    async def detect_drift(ctx: StepContext) -> Dict[str, Any]:
        """Run DriftDetector on the engine's current vs baseline metrics.

        Gracefully skips if no metrics are provided.
        """
        engine_id = ctx.inputs["engine_id"]

        # Check if any drift detection inputs were provided
        has_metrics = any(
            ctx.inputs.get(k)
            for k in (
                "current_template_dist", "baseline_template_dist",
                "recent_success_rate", "baseline_success_rate",
                "recent_yield", "baseline_yield",
                "recent_avg_duration_ms", "baseline_avg_duration_ms",
            )
        )

        if not has_metrics:
            return {
                "engine_id": engine_id,
                "drift_detected": False,
                "drift_magnitude": 0.0,
                "drift_report": {},
                "skipped": True,
            }

        report = lm.mutator.drift_detector.detect(
            engine_id,
            current_template_dist=ctx.inputs.get("current_template_dist"),
            baseline_template_dist=ctx.inputs.get("baseline_template_dist"),
            recent_success_rate=ctx.inputs.get("recent_success_rate", 1.0),
            baseline_success_rate=ctx.inputs.get("baseline_success_rate", 1.0),
            recent_yield=ctx.inputs.get("recent_yield", 0.0),
            baseline_yield=ctx.inputs.get("baseline_yield", 0.0),
            recent_avg_duration_ms=ctx.inputs.get("recent_avg_duration_ms", 0),
            baseline_avg_duration_ms=ctx.inputs.get("baseline_avg_duration_ms", 0),
        )

        return {
            "engine_id": engine_id,
            "drift_detected": report.drifted,
            "drift_magnitude": report.magnitude,
            "drift_report": report.to_dict(),
        }

    # ------------------------------------------------------------------
    # Step 2: evaluate_trigger
    # ------------------------------------------------------------------

    async def evaluate_trigger(ctx: StepContext) -> Dict[str, Any]:
        """Check mutation conditions (yield + entropy thresholds + drift)."""
        engine_id = ctx.inputs["engine_id"]
        yield_score = ctx.inputs.get("yield_score", 1.0)
        entropy_score = ctx.inputs.get("entropy_score", 0.0)
        drift_detected = ctx.inputs.get("drift_detected", False)
        drift_magnitude = ctx.inputs.get("drift_magnitude", 0.0)
        drift_report = ctx.inputs.get("drift_report", {})

        should_entropy_mutate = lm.mutator.should_mutate(yield_score, entropy_score)
        should_drift_mutate = drift_detected

        if not should_entropy_mutate and not should_drift_mutate:
            return {
                "engine_id": engine_id,
                "mutation_needed": False,
                "reason": "thresholds not met and no drift",
            }

        # Determine strategy
        if should_drift_mutate and not should_entropy_mutate:
            strategy = MutationStrategy.DRIFT_CORRECTION
        else:
            strategy = MutationStrategy.ENTROPY_YIELD

        trigger = MutationTrigger(
            strategy=strategy,
            yield_score=yield_score,
            entropy_score=entropy_score,
            yield_threshold=lm.mutator.yield_threshold,
            entropy_threshold=lm.mutator.entropy_threshold,
            drift_magnitude=drift_magnitude,
            reasoning=(
                f"yield={yield_score:.4f}, entropy={entropy_score:.4f}, "
                f"drift={drift_magnitude:.4f}"
            ),
        )

        return {
            "engine_id": engine_id,
            "mutation_needed": True,
            "trigger": trigger.to_dict(),
            "strategy": strategy.value,
        }

    # ------------------------------------------------------------------
    # Step 3: compute_mutation
    # ------------------------------------------------------------------

    async def compute_mutation(ctx: StepContext) -> Dict[str, Any]:
        """Apply EvolutionaryMutator to produce new cDNA."""
        engine_id = ctx.inputs["engine_id"]
        mutation_needed = ctx.inputs.get("mutation_needed", False)

        if not mutation_needed:
            return {"engine_id": engine_id, "mutated": False}

        current = lm.get_current(engine_id)
        if current is None:
            return {
                "engine_id": engine_id,
                "mutated": False,
                "error": f"no cDNA record for {engine_id}",
            }

        trigger_data = ctx.inputs.get("trigger", {})
        trigger = MutationTrigger(
            strategy=MutationStrategy(trigger_data.get("strategy", "entropy_yield")),
            yield_score=trigger_data.get("yield_score", 0.0),
            entropy_score=trigger_data.get("entropy_score", 0.0),
            yield_threshold=trigger_data.get("yield_threshold", 0.5),
            entropy_threshold=trigger_data.get("entropy_threshold", 0.65),
            drift_magnitude=trigger_data.get("drift_magnitude", 0.0),
            reasoning=trigger_data.get("reasoning", ""),
        )

        new_record, proof = lm.mutator.mutate(current, trigger)

        return {
            "engine_id": engine_id,
            "mutated": True,
            "before_cdna": current.cdna,
            "after_cdna": new_record.cdna,
            "before_generation": current.generation,
            "after_generation": new_record.generation,
            "before_spec_version": current.spec_version,
            "after_spec_version": new_record.spec_version,
            "trigger": trigger.to_dict(),
        }

    # ------------------------------------------------------------------
    # Step 4: seal_proof
    # ------------------------------------------------------------------

    async def seal_proof(ctx: StepContext) -> Dict[str, Any]:
        """Seal the mutation proof and update LineageManager state."""
        engine_id = ctx.inputs["engine_id"]
        mutated = ctx.inputs.get("mutated", False)

        if not mutated:
            return {"engine_id": engine_id, "sealed": False}

        trigger_data = ctx.inputs.get("trigger", {})
        trigger = MutationTrigger(
            strategy=MutationStrategy(trigger_data.get("strategy", "entropy_yield")),
            yield_score=trigger_data.get("yield_score", 0.0),
            entropy_score=trigger_data.get("entropy_score", 0.0),
            yield_threshold=trigger_data.get("yield_threshold", 0.5),
            entropy_threshold=trigger_data.get("entropy_threshold", 0.65),
            drift_magnitude=trigger_data.get("drift_magnitude", 0.0),
            reasoning=trigger_data.get("reasoning", ""),
        )

        current = lm.get_current(engine_id)
        if current is None:
            return {"engine_id": engine_id, "sealed": False, "error": "no record"}

        # Apply through LineageManager (seals, chains, fires callback)
        proof = lm._apply_mutation(current, trigger)

        return {
            "engine_id": engine_id,
            "sealed": True,
            "new_cdna": proof.after_cdna,
            "new_generation": proof.after_generation,
            "new_spec_version": proof.after_spec_version,
            "proof_hash": proof.proof_hash,
            "block_ref": proof.block_ref,
        }

    # ------------------------------------------------------------------
    # Step 5: notify_mutation
    # ------------------------------------------------------------------

    async def notify_mutation(ctx: StepContext) -> Dict[str, Any]:
        """Record that the mutation lifecycle completed.

        The actual on_mutation callback fires inside _apply_mutation
        (seal_proof step).  This step exists for proof completeness —
        auditors can verify the full 5-step chain executed.
        """
        engine_id = ctx.inputs["engine_id"]
        sealed = ctx.inputs.get("sealed", False)

        return {
            "engine_id": engine_id,
            "notified": sealed,
            "callback_registered": lm._on_mutation is not None,
            "lifecycle_complete": True,
        }

    return {
        "detect_drift": detect_drift,
        "evaluate_trigger": evaluate_trigger,
        "compute_mutation": compute_mutation,
        "seal_proof": seal_proof,
        "notify_mutation": notify_mutation,
    }
