# ============================================================
# src/accountable_record/pipeline/runner.py
# ============================================================
"""Pipeline runner (orchestration)."""

from collections.abc import Iterable

from accountable_record.pipeline.context import (
    PipelineRunContext,
    PipelineState,
)
from accountable_record.pipeline.diagnostics import Diagnostics
from accountable_record.pipeline.stages import (
    PipelineStage,
    get_stage,
    stages_through,
)

__all__ = [
    "run_stage_sequence_state",
    "run_through",
    "run_through_state",
]


def run_through(target: str, context: PipelineRunContext) -> int:
    """Run through a target stage and return only the exit status."""
    status, _state = run_through_state(target, context)
    return status


def run_through_state(
    target: str,
    context: PipelineRunContext,
) -> tuple[int, PipelineState]:
    """Run through a target stage and return status plus final state.

    OBS:
        Runs the contiguous prefix of stages up to and including target.
        This is the verifier path: every stage from the first through the
        target runs in declared order.
    """
    return _run_stages(stages_through(target), context)


def run_stage_sequence_state(
    stage_codes: Iterable[str],
    context: PipelineRunContext,
) -> tuple[int, PipelineState]:
    """Run an explicit, possibly non-contiguous sequence of stages.

    WHY:
        Write-intent commands need to establish state through some stages while
        intentionally skipping others. For example, write-lock must run the
        read-only prerequisites that produce generated state, but must skip
        s070, because s070 verifies an existing lock and the lock does not yet
        exist on a write run. Such commands route through this function so they
        do not duplicate stage execution, diagnostics printing, halt behavior,
        or state threading.

    OBS:
        The caller supplies stage codes in the exact order they should run. This
        function does not reorder them and does not enforce contiguity; the
        contiguous verifier path remains run_through_state. Each code is
        resolved against the registry, so an unknown code raises ValueError
        before any stage runs.

    Args:
        stage_codes: Stage codes (e.g. "s010", "s060", "s080") in run order.
        context: Fixed run input.

    Returns:
        Exit status and the final accumulated state, with the same halt-on-
        failure behavior as the contiguous runner.
    """
    stages = tuple(get_stage(code) for code in stage_codes)
    return _run_stages(stages, context)


def _run_stages(
    stages: tuple[PipelineStage, ...],
    context: PipelineRunContext,
) -> tuple[int, PipelineState]:
    """Execute an ordered tuple of stages, threading and printing state."""
    state = PipelineState.initial(context)

    for stage_ in stages:
        before = state.diagnostics

        state = stage_.transform(state)

        _print_stage_result(stage_, state, before)

        if state.failed_stage == stage_.code:
            print(f"Pipeline stopped at {stage_.code} ({stage_.concern.value}).")
            return 1, state

    return 0, state


def _print_stage_result(
    stage_: PipelineStage,
    state: PipelineState,
    before: Diagnostics,
) -> None:
    """Print one stage's status and the diagnostics it added this step."""
    failed_here = state.failed_stage == stage_.code
    status = "FAIL" if failed_here else "ok"
    print(f"[{stage_.code} {stage_.concern.value}] {status}  {stage_.description}")

    added = state.diagnostics.items[len(before.items) :]
    for diagnostic in added:
        print(
            f"    {diagnostic.severity.value}: {diagnostic.code} {diagnostic.message}"
        )
