"""Hyperparameter scan over dropout x n_blocks x hidden_dim.

Runs `giant train` sequentially (this machine has a single GPU) for every
combination, plus one extra run at the default architecture with a higher
learning rate. Runs are shuffled so the parameter space gets coarse coverage
early rather than exhausting one corner of the grid first.

See `uv run dwarf hparam-scan --help` for the CLI.
"""

import csv
import itertools
import os
import random
import subprocess
import time
from pathlib import Path

DATA_DEFAULT = "/home/lars/geant_steps/train"
SCAN_DIR_DEFAULT = "checkpoints/scan"
EPOCHS = 50
DEFAULT_LR = 3e-4

DROPOUTS = [0.0, 0.1]
N_BLOCKS = [5, 6, 7, 8]
HIDDEN_DIMS = [256, 512, 1024]

EXTRA_LR_RUN = {"hidden_dim": 256, "n_blocks": 6, "dropout": 0.1, "lr": 1e-3}

_SUMMARY_FIELDS = [
    "name",
    "hidden_dim",
    "n_blocks",
    "dropout",
    "lr",
    "epochs_completed",
    "final_val_loss",
    "best_val_loss",
    "wall_time_s",
]


def build_runs(seed: int) -> list[dict]:
    runs = [
        {"hidden_dim": h, "n_blocks": n, "dropout": d, "lr": DEFAULT_LR}
        for d, n, h in itertools.product(DROPOUTS, N_BLOCKS, HIDDEN_DIMS)
    ]
    runs.append(dict(EXTRA_LR_RUN))
    random.Random(seed).shuffle(runs)
    return runs


def run_name(run: dict) -> str:
    return f"h{run['hidden_dim']}_n{run['n_blocks']}_d{run['dropout']}_lr{run['lr']}"


def last_completed_epoch(metrics_path: Path) -> int:
    if not metrics_path.exists():
        return 0
    with open(metrics_path, newline="") as f:
        rows = list(csv.DictReader(f))
    if not rows:
        return 0
    return int(rows[-1]["epoch"])


def final_metrics(metrics_path: Path) -> tuple[int, float, float]:
    with open(metrics_path, newline="") as f:
        rows = list(csv.DictReader(f))
    epochs_completed = int(rows[-1]["epoch"])
    final_val_loss = float(rows[-1]["val_loss"])
    best_val_loss = min(float(r["val_loss"]) for r in rows)
    return epochs_completed, final_val_loss, best_val_loss


def append_summary(summary_path: Path, row: dict) -> None:
    write_header = not summary_path.exists()
    with open(summary_path, "a", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=_SUMMARY_FIELDS)
        if write_header:
            writer.writeheader()
        writer.writerow(row)


def run_hparam_scan(
    data: str = DATA_DEFAULT,
    scan_dir: str = SCAN_DIR_DEFAULT,
    seed: int = 0,
    dry_run: bool = False,
) -> None:
    runs = build_runs(seed)
    scan_dir_path = Path(scan_dir)

    if dry_run:
        for i, run in enumerate(runs, 1):
            print(f"[{i}/{len(runs)}] {run_name(run)}")
        return

    scan_dir_path.mkdir(parents=True, exist_ok=True)
    summary_path = scan_dir_path / "scan_summary.csv"

    env = os.environ.copy()
    env["TQDM_DISABLE"] = "1"

    for i, run in enumerate(runs, 1):
        name = run_name(run)
        out_dir = scan_dir_path / name
        metrics_path = out_dir / "metrics.csv"
        last_ckpt = out_dir / "last.pt"

        completed = last_completed_epoch(metrics_path)
        if completed >= EPOCHS:
            print(f"[{i}/{len(runs)}] {name} — already complete, skipping")
            continue

        out_dir.mkdir(parents=True, exist_ok=True)
        cmd = [
            "giant",
            "train",
            data,
            "--mode",
            "flow",
            "--epochs",
            str(EPOCHS),
            "--batch-size",
            "auto",
            "--hidden-dim",
            str(run["hidden_dim"]),
            "--n-blocks",
            str(run["n_blocks"]),
            "--dropout",
            str(run["dropout"]),
            "--lr",
            str(run["lr"]),
            "--out",
            str(out_dir),
        ]
        if last_ckpt.exists():
            cmd += ["--resume", str(last_ckpt)]
            print(f"[{i}/{len(runs)}] {name} — resuming from epoch {completed}")
        else:
            print(f"[{i}/{len(runs)}] {name} — starting")

        start = time.monotonic()
        try:
            with open(out_dir / "train.log", "a") as log:
                subprocess.run(cmd, env=env, stdout=log, stderr=subprocess.STDOUT)
        except KeyboardInterrupt:
            print(
                f"\ninterrupted during {name} — re-run this script to resume "
                f"(checkpoint/resume is handled by `giant train` itself)"
            )
            return
        wall_time_s = time.monotonic() - start

        if metrics_path.exists():
            epochs_completed, final_val_loss, best_val_loss = final_metrics(
                metrics_path
            )
            append_summary(
                summary_path,
                {
                    "name": name,
                    "hidden_dim": run["hidden_dim"],
                    "n_blocks": run["n_blocks"],
                    "dropout": run["dropout"],
                    "lr": run["lr"],
                    "epochs_completed": epochs_completed,
                    "final_val_loss": final_val_loss,
                    "best_val_loss": best_val_loss,
                    "wall_time_s": round(wall_time_s, 1),
                },
            )
            print(
                f"[{i}/{len(runs)}] {name} — val_loss {final_val_loss:.4f} "
                f"({wall_time_s:.1f}s)"
            )
        else:
            print(
                f"[{i}/{len(runs)}] {name} — no metrics.csv produced, check train.log"
            )
