"""Run directories: the manifest beside each `trials.jsonl`.

The JSONL log is the source of truth for *trials* (ADR-0006), but a trial
record does not know which file arm A came from, what command produced it, or
how many times the resulting dataset has since been re-analyzed. That context
lives here, in a small sidecar, so `ablate report` can reproduce a run's header
without re-running anything.

The re-analysis counter is not bookkeeping. `ablate report --last --margin ...`
is free and instant, which makes it trivially easy to re-analyze a dataset
until it says something you like — sequential testing without correction. The
tool cannot prevent that, but it can refuse to hide it, so the count is
recorded and printed (docs/05-statistics.md, "peeking / optional stopping").
"""

from __future__ import annotations

import json
from dataclasses import asdict, dataclass, field
from pathlib import Path

from ablate.config.variant import ConfigVariant
from ablate.errors import AblateError

MANIFEST = "manifest.json"
TRIALS = "trials.jsonl"


@dataclass
class VariantRef:
    """Enough of a `ConfigVariant` to rebuild the report header.

    The config *text* is stored so a report can be regenerated after the file
    on disk has been edited — which is the normal case, since the whole point
    of running this is to then change the config.
    """

    id: str
    source: str
    text: str | None

    def to_variant(self) -> ConfigVariant:
        return ConfigVariant(id=self.id, text=self.text, source=self.source)


@dataclass
class RunManifest:
    run_id: str
    command: str
    agent: str
    agent_version: str | None
    model: str | None
    variants: list[VariantRef]
    n_tasks: int
    n_trials: int
    seed: int | None = None
    partial: str | None = None
    analyses: int = 1
    extends: str | None = field(default=None)

    @classmethod
    def load(cls, run_dir: Path) -> RunManifest:
        path = run_dir / MANIFEST
        if not path.exists():
            raise AblateError(
                f"no manifest in {run_dir} — this run predates `ablate report`, "
                f"or the directory is not an ablate run"
            )
        raw = json.loads(path.read_text())
        raw["variants"] = [VariantRef(**v) for v in raw["variants"]]
        return cls(**raw)

    def save(self, run_dir: Path) -> None:
        run_dir.mkdir(parents=True, exist_ok=True)
        (run_dir / MANIFEST).write_text(json.dumps(asdict(self), indent=2) + "\n")

    def to_variants(self) -> list[ConfigVariant]:
        return [v.to_variant() for v in self.variants]


def runs_root(repo: Path) -> Path:
    return repo / ".ablate" / "runs"


def list_runs(repo: Path) -> list[Path]:
    """Run directories, oldest first. Run ids are UTC timestamps, so the
    lexical sort is chronological."""
    root = runs_root(repo)
    if not root.exists():
        return []
    return sorted(d for d in root.iterdir() if d.is_dir() and (d / TRIALS).exists())


def resolve_run(repo: Path, run: str | None) -> Path:
    """Find a run by id, by path, or `None` for the most recent."""
    available = list_runs(repo)
    if not available:
        raise AblateError(
            "no stored runs found. Run `ablate compare` first — `ablate report` "
            "re-analyzes an existing run and never calls the agent."
        )

    if run is None:
        return available[-1]

    direct = Path(run)
    if direct.is_dir() and (direct / TRIALS).exists():
        return direct

    match = [d for d in available if d.name == run]
    if not match:
        known = ", ".join(d.name for d in available[-5:])
        raise AblateError(f"no run named {run!r}. Most recent: {known}")
    return match[0]
