"""Pattern-first multi-model skill runtime."""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from inspect import signature
from typing import Any

from ..core.agent import BioAgent, BioAgentConfig
from ..core.types import ActionProtein, Signal
from ..memory.bitemporal import BiTemporalFact, BiTemporalMemory, BiTemporalQuery
from ..utils import _call_arity
from ..organelles.nucleus import Nucleus
from ..state.metabolism import ATP_Store
from .types import (
    InterventionKind,
    RunContext,
    SkillRunResult,
    SkillRuntimeComponent,
    SkillStage,
    SkillStageResult,
    StateConflictError,
    SubstrateView,
    TelemetryEvent,
    WATCHER_STATE_KEY,
    WatcherIntervention,
)


def _merge_parallel_results(
    state: dict[str, Any],
    stage_outputs: dict[str, Any],
    stage_results: list[SkillStageResult],
    snap: dict[str, Any],
    per_stage: dict[str, tuple[dict, dict, list, str]],
) -> None:
    """Merge isolated parallel stage results back into shared state.

    Each parallel stage ran with its own copy of ``state`` and
    ``stage_outputs``. This function merges the results, detecting
    conflicts when two stages wrote different values to the same key.
    """
    mutations: dict[str, list[tuple[str, Any]]] = {}

    for stage_name, (s_state, s_outputs, s_results, _) in per_stage.items():
        # Collect stage outputs (no conflicts — each stage writes its own name)
        if stage_name in s_outputs:
            stage_outputs[stage_name] = s_outputs[stage_name]
        stage_results.extend(s_results)

        # Track state mutations (keys that changed vs snapshot)
        for key, value in s_state.items():
            # Each stage writes state[stage.name] = output — handled below
            if key == "last_stage" or key in per_stage:
                continue
            # Framework-internal keys (_-prefixed): merge deltas only.
            # Lists: append only items added since snapshot.
            # Scalars: apply only if changed from snapshot.
            if key.startswith("_"):
                snap_val = snap.get(key)
                if isinstance(value, list):
                    # Treat missing snapshot as empty list baseline
                    baseline = snap_val if isinstance(snap_val, list) else []
                    new_items = value[len(baseline):]
                    if new_items:
                        current = state.get(key, [])
                        if isinstance(current, list):
                            state[key] = current + new_items
                        else:
                            state[key] = new_items
                elif value != snap_val:
                    state[key] = value  # changed from snapshot
                continue
            if key not in snap or snap[key] != value:
                mutations.setdefault(key, []).append((stage_name, value))

    # Apply mutations with conflict detection
    for key, writers in mutations.items():
        if len(writers) > 1:
            values = {str(v) for _, v in writers}
            if len(values) > 1:
                names = [n for n, _ in writers]
                raise StateConflictError(
                    f"Parallel stages {names} wrote conflicting values "
                    f"for key '{key}'"
                )
        state[key] = writers[0][1]

    # Merge per-stage output keys in declared order (deterministic)
    for stage_name, (s_state, _, _, _) in per_stage.items():
        if stage_name in s_state:
            state[stage_name] = s_state[stage_name]

    # Set last_stage to the last declared stage in the group
    state["last_stage"] = list(per_stage.keys())[-1]


def _normalize_stage_groups(
    stages: list | tuple,
) -> tuple[tuple[SkillStage, ...], ...]:
    """Normalize mixed flat/grouped stage specs to uniform groups.

    Accepts:
      - Flat list of stages: ``[s1, s2, s3]`` → ``((s1,), (s2,), (s3,))``
      - Grouped list: ``[[s1, s2], [s3]]`` → ``((s1, s2), (s3,))``
      - Mixed: ``[s1, [s2, s3], s4]`` → ``((s1,), (s2, s3), (s4,))``
    """
    groups: list[tuple[SkillStage, ...]] = []
    for item in stages:
        if isinstance(item, (list, tuple)) and not isinstance(item, SkillStage):
            groups.append(tuple(item))
        else:
            groups.append((item,))
    return tuple(groups)


def _coerce_handler_output(value: Any, stage_name: str) -> ActionProtein:
    if isinstance(value, ActionProtein):
        value.source_agent = value.source_agent or stage_name
        return value
    # Convention: handler can signal action_type via _action_type key (e.g., CLI handlers)
    # Read without mutating — the caller may reuse the same dict object.
    action_type = "EXECUTE"
    if isinstance(value, dict) and "_action_type" in value:
        action_type = value["_action_type"]
        value = {k: v for k, v in value.items() if k != "_action_type"}
    return ActionProtein(
        action_type=action_type,
        payload=value,
        confidence=1.0,
        source_agent=stage_name,
        metadata={"provider": "deterministic", "model": "deterministic-handler"},
    )


def _normalize_mode(mode: str) -> str:
    return mode.strip().lower().replace("-", "_")


def _coerce_fact_events(
    raw: Any,
    default_source: str,
    default_tags: tuple[str, ...],
) -> list[dict[str, Any]]:
    """Normalize a fact_extractor return value into a list of event dicts."""
    if raw is None:
        return []
    if isinstance(raw, list):
        out: list[dict[str, Any]] = []
        for item in raw:
            out.extend(_coerce_fact_events(item, default_source, default_tags))
        return out
    if isinstance(raw, BiTemporalFact):
        return [{
            "op": "assert",
            "subject": raw.subject,
            "predicate": raw.predicate,
            "value": raw.value,
            "valid_from": raw.valid_from,
            "recorded_from": raw.recorded_from,
            "source": raw.source or default_source,
            "confidence": raw.confidence,
            "tags": raw.tags or default_tags,
        }]
    if isinstance(raw, tuple) and len(raw) == 3:
        subject, predicate, value = raw
        return [{
            "op": "assert",
            "subject": subject,
            "predicate": predicate,
            "value": value,
            "source": default_source,
            "tags": default_tags,
        }]
    if isinstance(raw, dict):
        event = dict(raw)
        event.setdefault("op", "assert")
        event.setdefault("source", default_source)
        event.setdefault("tags", default_tags)
        return [event]
    raise TypeError(
        f"fact_extractor returned unsupported type {type(raw).__name__}. "
        "Expected dict, list, BiTemporalFact, tuple(subject, predicate, value), or None."
    )


@dataclass
class TelemetryProbe:
    """
    Built-in runtime component that records stage-level telemetry.

    By default it also writes the events into a namespaced shared-state key,
    so downstream stages can inspect what happened without colliding with
    application-owned fields.
    """

    state_key: str = "_operon_telemetry"
    events: list[TelemetryEvent] = field(default_factory=list)

    def _append(self, shared_state: dict[str, Any], event: TelemetryEvent) -> None:
        self.events.append(event)
        shared_state.setdefault(self.state_key, []).append(
            {
                "kind": event.kind,
                "stage_name": event.stage_name,
                **event.payload,
            }
        )

    def on_run_start(self, task: str, shared_state: dict[str, Any]) -> None:
        import copy
        payload: dict[str, Any] = {"task": task}
        # Capture organism config if injected by SkillOrganism.run().
        # Deep-copy to make the telemetry record immutable.
        org_config = shared_state.get("_organism_config")
        if isinstance(org_config, dict):
            payload["organism_config"] = copy.deepcopy(org_config)
        self._append(
            shared_state,
            TelemetryEvent(
                kind="run_start",
                stage_name=None,
                payload=payload,
            ),
        )

    def on_stage_start(
        self,
        stage: SkillStage,
        shared_state: dict[str, Any],
        stage_outputs: dict[str, Any],
    ) -> None:
        self._append(
            shared_state,
            TelemetryEvent(
                kind="stage_start",
                stage_name=stage.name,
                payload={
                    "role": stage.role,
                    "mode": stage.mode,
                    "known_outputs": sorted(stage_outputs.keys()),
                },
            ),
        )

    def on_stage_result(
        self,
        stage: SkillStage,
        result: SkillStageResult,
        shared_state: dict[str, Any],
        stage_outputs: dict[str, Any],
    ) -> None:
        self._append(
            shared_state,
            TelemetryEvent(
                kind="stage_result",
                stage_name=stage.name,
                payload={
                    "provider": result.provider,
                    "model": result.model,
                    "model_alias": result.model_alias,
                    "tokens_used": result.tokens_used,
                    "latency_ms": result.latency_ms,
                    "action_type": result.action_type,
                },
            ),
        )

    def on_run_complete(self, result: SkillRunResult, shared_state: dict[str, Any]) -> None:
        self._append(
            shared_state,
            TelemetryEvent(
                kind="run_complete",
                stage_name=None,
                payload={
                    "final_output": result.final_output,
                    "stages": [stage.stage_name for stage in result.stage_results],
                },
            ),
        )

    def summary(self) -> dict[str, Any]:
        stage_results = [e for e in self.events if e.kind == "stage_result"]
        return {
            "events": len(self.events),
            "stages": [e.stage_name for e in stage_results],
            "total_tokens": sum(e.payload.get("tokens_used", 0) for e in stage_results),
            "total_latency_ms": sum(e.payload.get("latency_ms", 0.0) for e in stage_results),
        }


@dataclass
class SkillOrganism:
    """Composable multi-stage runtime with model-tier routing."""

    stages: tuple[SkillStage, ...]
    nuclei: dict[str, Nucleus]
    budget: ATP_Store
    fast_alias: str = "fast"
    deep_alias: str = "deep"
    components: tuple[SkillRuntimeComponent, ...] = ()
    halt_on_block: bool = True
    substrate: BiTemporalMemory | None = None
    stage_groups: tuple[tuple[SkillStage, ...], ...] = ()
    _agents: dict[str, BioAgent] = field(default_factory=dict, init=False)

    def __post_init__(self) -> None:
        for stage in self.stages:
            if stage.handler is not None:
                continue
            alias = self._resolve_alias(stage)
            nucleus = self.nuclei[alias]
            self._agents[stage.name] = BioAgent(
                name=stage.name,
                role=stage.role,
                atp_store=self.budget,
                config=BioAgentConfig(
                    nucleus=nucleus,
                    instructions=stage.instructions,
                    provider_config=stage.provider_config,
                    tool_mitochondria=stage.tools,
                    silent=True,
                )
            )

    def collect_certificates(self) -> list:
        """Gather certificates from all certifiable components.

        Checks the budget (ATP_Store) and any components that expose
        ``certify()`` (structural) or ``certify_behavior()`` (behavioral)
        methods.  Returns a list of :class:`Certificate` objects.
        """
        from ..core.certificate import Certificate

        certs: list[Certificate] = []
        if hasattr(self.budget, "certify"):
            try:
                certs.append(self.budget.certify())
            except (ValueError, TypeError):
                pass
        for component in self.components:
            if hasattr(component, "certify"):
                try:
                    certs.append(component.certify())
                except (ValueError, TypeError):
                    pass
            if hasattr(component, "certify_behavior"):
                try:
                    result = component.certify_behavior()
                    if result is None:
                        pass
                    elif isinstance(result, list):
                        certs.extend(result)
                    else:
                        certs.append(result)
                except (ValueError, TypeError):
                    pass
        return certs

    def run_single_stage(
        self,
        stage: SkillStage,
        task: str,
        state: dict[str, Any] | RunContext,
        stage_outputs: dict[str, Any],
        stage_results: list[SkillStageResult],
    ) -> str:
        """Execute one stage with full component hooks and intervention handling.

        This is the core per-stage logic extracted from :meth:`run`. Both
        ``run()`` and the LangGraph compiler call this method, ensuring
        identical behavior.

        Parameters
        ----------
        state:
            Accepts ``dict`` or ``RunContext``.  A plain dict is
            automatically wrapped in a ``RunContext`` with the watcher
            key resolved from the organism's components.

        Returns
        -------
        str
            ``"continue"`` if the pipeline should proceed to the next stage,
            ``"halt"`` if the pipeline should stop (watcher HALT or
            halt_on_block), or ``"blocked"`` if a pre-stage gate blocked
            execution.
        """
        # Normalize to RunContext if caller passed a plain dict.
        # We update the original dict in-place so mutations are visible
        # to the caller.
        _original_dict = None
        if not isinstance(state, RunContext):
            _original_dict = state
            watcher_key = WATCHER_STATE_KEY
            for component in self.components:
                cfg = getattr(component, "config", None)
                sk = getattr(cfg, "state_key", None)
                if sk is not None:
                    watcher_key = sk
                    break
            state = RunContext(state, watcher_key=watcher_key)
        try:
            return self._execute_single_stage(
                stage, task, state, stage_outputs, stage_results,
            )
        finally:
            # Sync RunContext mutations back to the caller's original dict
            if _original_dict is not None:
                _original_dict.clear()
                _original_dict.update(state)

    def _execute_single_stage(
        self,
        stage: SkillStage,
        task: str,
        state: RunContext,
        stage_outputs: dict[str, Any],
        stage_results: list[SkillStageResult],
    ) -> str:
        """Inner implementation of run_single_stage (no dict sync)."""
        for component in self.components:
            component.on_stage_start(stage, state, stage_outputs)

        # --- Pre-stage intervention check (e.g. CertificateGate) ---
        _pre = state.pop(state._watcher_key, None)
        if isinstance(_pre, WatcherIntervention) and _pre.kind == InterventionKind.HALT:
            state["_blocked_by"] = _pre
            return "blocked"

        # --- Substrate read ---
        substrate_view: SubstrateView | None = None
        now: datetime | None = None
        if self.substrate is not None and stage.read_query is not None:
            now = datetime.now()
            substrate_view = self._build_substrate_view(
                stage, task, state, stage_outputs, now,
            )

        result = self._run_stage(stage, task, state, stage_outputs, substrate_view)
        stage_results.append(result)
        stage_outputs[stage.name] = result.output
        state[stage.name] = result.output
        state["last_stage"] = stage.name

        # --- Substrate write ---
        if self.substrate is not None and (
            stage.emit_output_fact or stage.fact_extractor
        ):
            if now is None:
                now = datetime.now()
            self._write_substrate_facts(
                stage, task, result, state, stage_outputs, now,
            )

        # Run non-watcher components first (e.g. VerifierComponent deposits
        # signals), then watcher last so it can collect all signals before
        # deciding interventions.
        for component in self.components:
            if not hasattr(component, '_decide_intervention'):
                component.on_stage_result(stage, result, state, stage_outputs)
        for component in self.components:
            if hasattr(component, '_decide_intervention'):
                component.on_stage_result(stage, result, state, stage_outputs)

        # --- Watcher intervention check ---
        _intervention = state.pop(state._watcher_key, None)
        if isinstance(_intervention, WatcherIntervention):
            if _intervention.kind == InterventionKind.RETRY:
                retry_result = self._run_stage(
                    stage, task, state, stage_outputs, substrate_view,
                )
                stage_results[-1] = retry_result
                stage_outputs[stage.name] = retry_result.output
                state[stage.name] = retry_result.output
                result = retry_result
            elif _intervention.kind == InterventionKind.ESCALATE:
                if stage.handler is None and self.deep_alias in self.nuclei:
                    try:
                        escalated = self._run_stage_escalated(
                            stage, task, state, stage_outputs, substrate_view,
                        )
                    except Exception as exc:
                        state["_escalation_error"] = str(exc)
                        escalated = None
                    if escalated is not None:
                        stage_results[-1] = escalated
                        stage_outputs[stage.name] = escalated.output
                        state[stage.name] = escalated.output
                        result = escalated
            elif _intervention.kind == InterventionKind.HALT:
                return "halt"

        if self.halt_on_block and result.action_type in {"BLOCK", "FAILURE"}:
            return "halt"

        return "continue"

    def _run_group(
        self,
        group: tuple[SkillStage, ...],
        task: str,
        state: dict[str, Any],
        stage_outputs: dict[str, Any],
        stage_results: list[SkillStageResult],
    ) -> str:
        """Execute a stage group — sequential if 1 stage, parallel if >1.

        Parallel stages get isolated state snapshots. Results are merged
        back with conflict detection after all stages complete.

        Returns ``"continue"`` if all stages succeed, ``"halt"`` if any
        stage halts (wait-and-cancel: running stages finish first).
        """
        if len(group) == 1:
            return self.run_single_stage(
                group[0], task, state, stage_outputs, stage_results,
            )

        import copy
        from concurrent.futures import ThreadPoolExecutor

        # Deep-copy state to isolate parallel stages from shared mutable objects
        snap = copy.deepcopy(dict(state))
        snap_outputs = copy.deepcopy(dict(stage_outputs))

        per_stage: dict[str, tuple[dict, dict, list, str]] = {}

        with ThreadPoolExecutor(max_workers=len(group)) as pool:
            ordered_futures = []
            for stage in group:
                s_state: dict[str, Any] = copy.deepcopy(snap)
                s_outputs: dict[str, Any] = copy.deepcopy(snap_outputs)
                s_results: list[SkillStageResult] = []
                future = pool.submit(
                    self.run_single_stage,
                    stage, task, s_state, s_outputs, s_results,
                )
                ordered_futures.append((future, stage, s_state, s_outputs, s_results))

            # Collect results in declared order, preserving actual decisions
            for future, stage, s_state, s_outputs, s_results in ordered_futures:
                decision = future.result()  # the real return from run_single_stage
                per_stage[stage.name] = (s_state, s_outputs, s_results, decision)

        # Merge results back
        _merge_parallel_results(
            state, stage_outputs, stage_results, snap, per_stage,
        )

        # Any halt → stop pipeline
        if any(d != "continue" for _, _, _, d in per_stage.values()):
            return "halt"
        return "continue"

    def run(
        self,
        task: str,
        shared_state: dict[str, Any] | None = None,
    ) -> SkillRunResult:
        # Preserve an incoming RunContext (keeps custom key configuration);
        # otherwise wrap in a new RunContext, resolving the watcher key from
        # any configured WatcherComponent so custom state_key works end-to-end.
        if isinstance(shared_state, RunContext):
            state = shared_state
        else:
            watcher_key = WATCHER_STATE_KEY
            for component in self.components:
                cfg = getattr(component, "config", None)
                sk = getattr(cfg, "state_key", None)
                if sk is not None:
                    watcher_key = sk
                    break
            state = RunContext(shared_state or {}, watcher_key=watcher_key)
        state.pop("_blocked_by", None)  # Clear stale pre-stage halt marker
        stage_outputs: dict[str, Any] = {}
        stage_results: list[SkillStageResult] = []

        # Inject organism metadata temporarily so TelemetryProbe can capture
        # it during on_run_start, then remove it to avoid leaking into
        # stage-visible shared_state or the returned SkillRunResult.
        state["_organism_config"] = {
            "stage_count": len(self.stages),
            "stage_names": [s.name for s in self.stages],
            "mode_assignments": {s.name: s.mode for s in self.stages},
            "certificate_theorems": [
                c.theorem for c in self.collect_certificates()
            ],
        }

        for component in self.components:
            component.on_run_start(task, state)

        state.pop("_organism_config", None)

        groups = self.stage_groups or tuple((s,) for s in self.stages)
        for group in groups:
            decision = self._run_group(
                group, task, state, stage_outputs, stage_results,
            )
            if decision != "continue":
                break

        final_output = stage_results[-1].output if stage_results else None
        run_result = SkillRunResult(
            task=task,
            final_output=final_output,
            stage_results=tuple(stage_results),
            shared_state=dict(state),
        )
        for component in self.components:
            component.on_run_complete(run_result, state)
        return SkillRunResult(
            task=run_result.task,
            final_output=run_result.final_output,
            stage_results=run_result.stage_results,
            shared_state=dict(state),
        )

    def _run_stage(
        self,
        stage: SkillStage,
        task: str,
        shared_state: dict[str, Any],
        stage_outputs: dict[str, Any],
        substrate_view: SubstrateView | None = None,
    ) -> SkillStageResult:
        if stage.handler is not None:
            protein = _coerce_handler_output(
                _call_arity(
                    stage.handler, task, dict(shared_state),
                    dict(stage_outputs), stage, substrate_view,
                ),
                stage.name,
            )
            return SkillStageResult(
                stage_name=stage.name,
                role=stage.role,
                output=protein.payload,
                model_alias="deterministic",
                provider=protein.metadata.get("provider", "deterministic"),
                model=protein.metadata.get("model", "deterministic-handler"),
                tokens_used=int(protein.metadata.get("tokens_used", 0)),
                latency_ms=float(protein.metadata.get("latency_ms", 0.0)),
                action_type=protein.action_type,
                metadata=dict(protein.metadata),
            )

        alias = self._resolve_alias(stage)
        agent = self._agents[stage.name]
        metadata: dict[str, Any] = {}
        if stage.include_shared_state and shared_state:
            metadata["shared_state"] = dict(shared_state)
        if stage.include_stage_outputs and stage_outputs:
            metadata["stage_outputs"] = dict(stage_outputs)
        if substrate_view is not None:
            metadata["substrate_view"] = substrate_view

        protein = agent.express(Signal(content=task, source="SkillOrganism", metadata=metadata))
        return SkillStageResult(
            stage_name=stage.name,
            role=stage.role,
            output=protein.payload,
            model_alias=alias,
            provider=str(protein.metadata.get("provider", alias)),
            model=str(protein.metadata.get("model", alias)),
            tokens_used=int(protein.metadata.get("tokens_used", 0)),
            latency_ms=float(protein.metadata.get("latency_ms", 0.0)),
            action_type=protein.action_type,
            metadata=dict(protein.metadata),
        )

    def _resolve_alias(self, stage: SkillStage) -> str:
        if stage.nucleus is not None:
            alias = stage.nucleus
        else:
            mode = _normalize_mode(stage.mode)
            if mode in {"fixed", "fast", "deterministic"}:
                alias = self.fast_alias
            elif mode in {"fuzzy", "deep", "reasoning"}:
                alias = self.deep_alias
            else:
                raise ValueError(
                    f"Unsupported stage mode '{stage.mode}'. "
                    "Use fixed, fast, deterministic, fuzzy, deep, or reasoning."
                )

        if alias not in self.nuclei:
            raise ValueError(
                f"Stage '{stage.name}' resolved to unknown nucleus alias '{alias}'"
            )
        return alias

    # -- Watcher helpers (Phase 3) ------------------------------------------

    def _run_stage_escalated(
        self,
        stage: SkillStage,
        task: str,
        shared_state: dict[str, Any],
        stage_outputs: dict[str, Any],
        substrate_view: SubstrateView | None = None,
    ) -> SkillStageResult:
        """Re-run an agent-backed stage using the deep nucleus."""
        deep_nucleus = self.nuclei[self.deep_alias]
        # Temporarily swap the agent to use the deep nucleus
        original_agent = self._agents.get(stage.name)
        self._agents[stage.name] = BioAgent(
            name=stage.name,
            role=stage.role,
            atp_store=self.budget,
            config=BioAgentConfig(
                nucleus=deep_nucleus,
                instructions=stage.instructions,
                provider_config=stage.provider_config,
                tool_mitochondria=stage.tools,
                silent=True,
            )
        )
        try:
            result = self._run_stage(stage, task, shared_state, stage_outputs, substrate_view)
            # Override model_alias — _run_stage resolves from stage.mode
            # which is still "fixed"/"fast", but we used the deep nucleus.
            return SkillStageResult(
                stage_name=result.stage_name,
                role=result.role,
                output=result.output,
                model_alias=self.deep_alias,
                provider=result.provider,
                model=result.model,
                tokens_used=result.tokens_used,
                latency_ms=result.latency_ms,
                action_type=result.action_type,
                metadata=result.metadata,
            )
        finally:
            # Restore original agent
            if original_agent is not None:
                self._agents[stage.name] = original_agent

    # -- Substrate helpers (Phase 2) -----------------------------------------

    def _build_substrate_view(
        self,
        stage: SkillStage,
        task: str,
        shared_state: dict[str, Any],
        stage_outputs: dict[str, Any],
        now: datetime,
    ) -> SubstrateView:
        assert self.substrate is not None
        query = stage.read_query

        if callable(query):
            resolved = _call_arity(query, task, shared_state, stage_outputs, self.substrate)
            if isinstance(resolved, BiTemporalQuery):
                facts = self.substrate.retrieve_known_at(
                    at=resolved.at_record or now,
                    subject=resolved.subject,
                    predicate=resolved.predicate,
                )
                return SubstrateView(facts=tuple(facts), query=resolved, record_time=now)
            if isinstance(resolved, list):
                return SubstrateView(facts=tuple(resolved), query=None, record_time=now)
            if isinstance(resolved, dict):
                facts = self.substrate.retrieve_known_at(
                    at=resolved.get("at_record", now),
                    subject=resolved.get("subject"),
                    predicate=resolved.get("predicate"),
                )
                return SubstrateView(facts=tuple(facts), query=query, record_time=now)
            raise TypeError(
                f"Stage '{stage.name}' read_query callable returned unsupported type "
                f"{type(resolved).__name__}."
            )

        # String → subject filter
        if isinstance(query, str):
            facts = self.substrate.retrieve_known_at(at=now, subject=query)
            return SubstrateView(
                facts=tuple(facts),
                query=BiTemporalQuery(subject=query, at_record=now),
                record_time=now,
            )

        return SubstrateView(facts=(), query=None, record_time=now)

    def _write_substrate_facts(
        self,
        stage: SkillStage,
        task: str,
        result: SkillStageResult,
        shared_state: dict[str, Any],
        stage_outputs: dict[str, Any],
        now: datetime,
    ) -> None:
        assert self.substrate is not None

        if stage.emit_output_fact:
            self.substrate.record_fact(
                subject=task,
                predicate=stage.name,
                value=result.output,
                valid_from=now,
                recorded_from=now,
                source=stage.name,
                tags=stage.fact_tags,
            )

        if stage.fact_extractor is not None:
            raw = _call_arity(
                stage.fact_extractor, task, shared_state, stage_outputs, stage, result,
            )
            events = _coerce_fact_events(raw, stage.name, stage.fact_tags)
            for event in events:
                self._apply_fact_event(event, now)

    def _apply_fact_event(self, event: dict[str, Any], now: datetime) -> None:
        assert self.substrate is not None
        op = event.get("op", "assert")
        if op == "assert":
            self.substrate.record_fact(
                subject=event["subject"],
                predicate=event["predicate"],
                value=event["value"],
                valid_from=event.get("valid_from", now),
                recorded_from=event.get("recorded_from", now),
                source=event.get("source", "unknown"),
                confidence=event.get("confidence", 1.0),
                tags=event.get("tags", ()),
            )
        elif op == "correct":
            self.substrate.correct_fact(
                old_fact_id=event["old_fact_id"],
                value=event["value"],
                valid_from=event.get("valid_from", now),
                recorded_from=event.get("recorded_from", now),
                source=event.get("source", "unknown"),
                confidence=event.get("confidence", 1.0),
                tags=event.get("tags", ()),
            )
        elif op == "invalidate":
            self.substrate.invalidate_fact(
                fact_id=event["fact_id"],
                recorded_to=event.get("recorded_to", now),
            )
        else:
            raise ValueError(
                f"Unknown fact event operation '{op}'. "
                "Expected 'assert', 'correct', or 'invalidate'."
            )


def skill_organism(
    *,
    stages: list | tuple,
    nuclei: dict[str, Nucleus] | None = None,
    fast_nucleus: Nucleus | None = None,
    deep_nucleus: Nucleus | None = None,
    fast_alias: str = "fast",
    deep_alias: str = "deep",
    components: list[SkillRuntimeComponent] | tuple[SkillRuntimeComponent, ...] = (),
    budget: ATP_Store | None = None,
    halt_on_block: bool = True,
    substrate: BiTemporalMemory | None = None,
) -> SkillOrganism:
    """
    Build a multi-stage organism that routes fixed vs fuzzy work by model tier.

    Fixed stages default to ``fast_alias`` and fuzzy stages default to
    ``deep_alias`` unless a stage pins itself to a specific nucleus alias.

    Stages can be flat (sequential) or grouped (parallel)::

        # Sequential: each stage runs one after another
        skill_organism(stages=[s1, s2, s3])

        # Parallel: stages in a group run concurrently
        skill_organism(stages=[[s1, s2], [s3], [s4, s5]])

        # Mixed: some sequential, some parallel
        skill_organism(stages=[s1, [s2, s3], s4])
    """

    stage_groups = _normalize_stage_groups(stages)
    # Flatten groups into a flat tuple for backward compatibility
    # (self.stages is used by LangGraph compiler, certificate extraction, etc.)
    stage_tuple = tuple(s for group in stage_groups for s in group)
    if not stage_tuple:
        raise ValueError("skill_organism requires at least one stage")

    seen_names: set[str] = set()
    for stage in stage_tuple:
        if stage.name in seen_names:
            raise ValueError(
                f"Duplicate stage name '{stage.name}'. "
                "Each SkillStage in an organism must have a unique name."
            )
        seen_names.add(stage.name)

    nuclei_map = dict(nuclei or {})
    if fast_nucleus is not None:
        nuclei_map[fast_alias] = fast_nucleus
    if deep_nucleus is not None:
        nuclei_map[deep_alias] = deep_nucleus

    for stage in stage_tuple:
        if stage.handler is not None:
            continue
        mode = _normalize_mode(stage.mode)
        if stage.nucleus is None and mode in {"fixed", "fast", "deterministic"} and fast_alias not in nuclei_map:
            raise ValueError(
                f"Stage '{stage.name}' needs a '{fast_alias}' nucleus for mode='{stage.mode}'"
            )
        if stage.nucleus is None and mode in {"fuzzy", "deep", "reasoning"} and deep_alias not in nuclei_map:
            raise ValueError(
                f"Stage '{stage.name}' needs a '{deep_alias}' nucleus for mode='{stage.mode}'"
            )
        if stage.nucleus is not None and stage.nucleus not in nuclei_map:
            raise ValueError(
                f"Stage '{stage.name}' refers to unknown nucleus alias '{stage.nucleus}'"
            )

    return SkillOrganism(
        stages=stage_tuple,
        nuclei=nuclei_map,
        budget=budget or ATP_Store(budget=1000, silent=True),
        fast_alias=fast_alias,
        deep_alias=deep_alias,
        components=tuple(components),
        halt_on_block=halt_on_block,
        substrate=substrate,
        stage_groups=stage_groups,
    )
