"""Sequential, resumable execution of an immutable paired trial plan."""

from __future__ import annotations

from pathlib import Path
from typing import Any, Callable, Mapping

from .io import (
    AtomicTrialWriter,
    TrialPayload,
    atomic_write_json,
    record_trial_failure,
    utc_now,
)
from .manifest import ensure_batch
from .plan import validate_trial_plan
from .validate import validate_trial_directory


TrialExecutor = Callable[[Mapping[str, Any]], TrialPayload | Mapping[str, Any]]


def _normalize_payload(value: TrialPayload | Mapping[str, Any]) -> TrialPayload:
    if isinstance(value, TrialPayload):
        return value
    if not isinstance(value, Mapping) or "samples" not in value:
        raise TypeError("executor must return TrialPayload or a mapping with samples")
    return TrialPayload(
        samples=value["samples"],
        events=value.get("events", ()),
        metadata=value.get("metadata", {}),
    )


def run_trial_plan(
    plan: Mapping[str, Any],
    batch_dir: Path,
    executor: TrialExecutor | None = None,
    *,
    resume: bool = True,
    dry_run: bool = False,
    continue_on_error: bool = True,
    max_trials: int | None = None,
    source_root: Path | None = None,
) -> dict[str, Any]:
    """Run trials in plan order and atomically commit successful artifacts."""
    validate_trial_plan(plan)
    selected = list(plan["trials"])
    if max_trials is not None:
        if max_trials < 0:
            raise ValueError("max_trials must be non-negative")
        selected = selected[:max_trials]
    if dry_run:
        return {
            "dry_run": True,
            "study_id": plan["study_id"],
            "split": plan["split"],
            "pair_count": len({trial["pair_id"] for trial in selected}),
            "trial_count": len(selected),
            "plan_hash": plan["plan_hash"],
        }
    if executor is None:
        raise ValueError("executor is required unless dry_run=True")

    batch_dir = Path(batch_dir)
    ensure_batch(batch_dir, plan, source_root=source_root)
    writer = AtomicTrialWriter(batch_dir)
    completed = 0
    skipped = 0
    failed = 0
    failures: list[dict[str, str]] = []
    for trial in selected:
        destination = writer.trial_dir(trial["trial_id"])
        if destination.exists():
            validation_errors = validate_trial_directory(destination, trial)
            if resume and not validation_errors:
                skipped += 1
                continue
            if validation_errors:
                raise RuntimeError(
                    f"existing trial {trial['trial_id']} is invalid: "
                    + "; ".join(validation_errors)
                )
            raise FileExistsError(f"trial already completed: {trial['trial_id']}")
        try:
            payload = _normalize_payload(executor(trial))
            writer.write(trial, payload)
            completed += 1
        except Exception as exc:
            failed += 1
            record_trial_failure(batch_dir, trial, exc)
            failures.append(
                {
                    "trial_id": trial["trial_id"],
                    "error_type": type(exc).__name__,
                    "error_message": str(exc),
                }
            )
            if not continue_on_error:
                raise
    summary = {
        "kind": "run_summary",
        "schema_version": plan["schema_version"],
        "plan_hash": plan["plan_hash"],
        "recorded_utc": utc_now(),
        "selected_trials": len(selected),
        "completed_trials": completed,
        "skipped_trials": skipped,
        "failed_trials": failed,
        "failures": failures,
    }
    atomic_write_json(batch_dir / "run_summary.json", summary)
    return summary
