"""Sequential runner: model × challenge × mode × sample, resumable.

Sequential by design — one GPU, and model swaps thrash memory. Every
attempt gets an isolated workspace and a JSONL session file; every result
is committed to SQLite as it lands. Degenerate output and transport
failures are recorded as errored results, never as scores.
"""

from __future__ import annotations

import time
import traceback
from pathlib import Path

from . import agent, client, db, grade as grading, oneshot, workspace
from .challenges import Challenge, load_all, set_hash
from .config import REPO_ROOT, Config
from .session import Session

RUNS_DIR = REPO_ROOT / ".lab" / "runs"


def run(config: Config, models: list[str] | None = None,
        challenge_ids: list[str] | None = None, note: str = "",
        run_id: int | None = None, db_path: Path | None = None,
        runs_dir: Path | None = None) -> int:
    """Execute (or resume, via run_id) a full run. Returns the run id."""
    challenges = load_all()
    if challenge_ids:
        challenges = [c for c in challenges if c.id in challenge_ids]
    roster = [config.models[m] for m in (models or sorted(config.models))]
    conn = db.connect(db_path) if db_path else db.connect()
    runs_dir = runs_dir or RUNS_DIR

    if run_id is None:
        run_id = db.create_run(conn, set_hash(challenges), config.raw, note)
    print(f"run {run_id}: {len(roster)} models × {len(challenges)} challenges × "
          f"{config.run.modes} × {config.run.samples} samples")

    for model in roster:
        for challenge in challenges:
            for mode in config.run.modes:
                for sample in range(config.run.samples):
                    if db.has_result(conn, run_id, model.alias, mode, challenge.id, sample):
                        continue
                    _attempt(conn, run_id, model, challenge, mode, sample, config, runs_dir)
    conn.close()
    return run_id


def _attempt(conn, run_id: int, model, challenge: Challenge, mode: str,
             sample: int, config: Config, runs_dir: Path) -> None:
    slug = f"{model.alias}/{mode}/{challenge.id}/s{sample}"
    root = runs_dir / str(run_id) / slug
    session_path = root / "session.jsonl"
    workspace.setup(root, challenge)
    started = time.monotonic()
    print(f"  {slug} ...", end="", flush=True)

    with Session(session_path) as session:
        session.log("start", run=run_id, model=model.alias, mode=mode,
                    challenge=challenge.id, sample=sample)
        try:
            if mode == "oneshot":
                oneshot.run(model, challenge, root, session, retries=config.run.request_retries)
            elif mode == "agentic":
                agent.run(model, challenge, root, session,
                          max_turns=config.run.max_agent_turns,
                          test_timeout=config.run.test_timeout_secs,
                          retries=config.run.request_retries)
            else:
                raise ValueError(f"unknown mode {mode!r}")
        except client.DegenerateOutput as err:
            session.log("degenerate", error=str(err))
            db.record_result(conn, run_id, model.alias, mode, challenge.id, sample,
                             status="degenerate", error=str(err),
                             wall_secs=time.monotonic() - started,
                             session_path=str(session_path))
            print(" DEGENERATE")
            return
        except Exception as err:  # transport errors, bugs — record, keep going
            session.log("error", error=str(err), trace=traceback.format_exc()[-4000:])
            db.record_result(conn, run_id, model.alias, mode, challenge.id, sample,
                             status="error", error=str(err),
                             wall_secs=time.monotonic() - started,
                             session_path=str(session_path))
            print(f" ERROR ({err})")
            return

        tests = workspace.run_tests(root, timeout=config.run.test_timeout_secs)
        result = grading.grade(challenge, root, tests)
        session.log("graded", total=result.total, tests_passed=tests.passed,
                    tests_failed=tests.failed, output=tests.output[-6000:])
        db.record_result(conn, run_id, model.alias, mode, challenge.id, sample,
                         status="ok", grade=result, tests=tests,
                         wall_secs=time.monotonic() - started,
                         session_path=str(session_path))
        print(f" {result.total}")
