from __future__ import annotations

import datetime
import json
import platform
import subprocess
import sys
import time
import traceback
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable, Optional

from .runner import run_case


def _now_iso_taipei() -> str:
    dt = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8)))
    return dt.isoformat(timespec="seconds")


def _git_commit(repo_root: Path) -> Optional[str]:
    try:
        out = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=str(repo_root))
        return out.decode().strip()
    except Exception:
        return None


def iter_case_dirs(cases_root: Path) -> list[Path]:
    """Find case directories under `cases_root` by locating `config.json`."""
    cases_root = cases_root.expanduser().resolve()
    out: list[Path] = []
    for cfg in cases_root.rglob("config.json"):
        out.append(cfg.parent)
    return sorted(set(out))


def _test_dir(case_dir: Path) -> Path:
    return case_dir / ".toughanimator" / "test"


def _write_json(path: Path, obj: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")


def _append_text(path: Path, s: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "a", encoding="utf-8") as f:
        f.write(s)


@dataclass
class TestResult:
    case_dir: Path
    status: str  # pass/fail/skip
    stage: str
    mode: str
    duration_sec: float
    started_at: str
    commit: Optional[str]
    error: Optional[dict] = None
    outputs: Optional[dict] = None


def run_one_case(
    case_dir: str | Path,
    *,
    stage: str = "convert",
    mode: str = "all",
    repo_root: Optional[Path] = None,
) -> TestResult:
    case_dir = Path(case_dir).expanduser().resolve()
    repo_root = (repo_root or Path.cwd()).resolve()

    t0 = time.time()
    started_at = _now_iso_taipei()
    commit = _git_commit(repo_root)

    tdir = _test_dir(case_dir)
    log_path = tdir / "latest.log"
    tb_path = tdir / "latest.traceback.txt"
    json_path = tdir / "latest.json"

    # reset log
    log_path.parent.mkdir(parents=True, exist_ok=True)
    log_path.write_text("", encoding="utf-8")
    _append_text(log_path, f"[START] {started_at}\ncase={case_dir}\nstage={stage} mode={mode}\n\n")

    try:
        if stage in {"parse", "convert", "sanity"}:
            res = run_case(case_dir, mode=mode)
            if not res.ok:
                raise RuntimeError(res.error or "run_case failed")

        outputs: Optional[dict] = None
        if stage == "sanity":
            # placeholder: user can implement stronger checks on output dirs (vtk/csv/png...)
            outputs = {"note": "sanity checks not configured"}

        dur = time.time() - t0
        tr = TestResult(
            case_dir=case_dir,
            status="pass",
            stage=stage,
            mode=mode,
            duration_sec=dur,
            started_at=started_at,
            commit=commit,
            outputs=outputs,
        )

    except Exception as e:
        dur = time.time() - t0
        tb = traceback.format_exc()
        tb_path.write_text(tb, encoding="utf-8")
        tr = TestResult(
            case_dir=case_dir,
            status="fail",
            stage=stage,
            mode=mode,
            duration_sec=dur,
            started_at=started_at,
            commit=commit,
            error={
                "type": type(e).__name__,
                "message": str(e),
                "traceback_file": str(tb_path.relative_to(case_dir)),
            },
        )

    payload = {
        "case_id": case_dir.name,
        "case_dir": str(case_dir),
        "status": tr.status,
        "stage": tr.stage,
        "mode": tr.mode,
        "started_at": tr.started_at,
        "duration_sec": round(tr.duration_sec, 6),
        "toughanimator_commit": tr.commit,
        "python": sys.version.split()[0],
        "platform": platform.platform(),
        "error": tr.error,
        "outputs": tr.outputs,
    }
    _write_json(json_path, payload)
    _append_text(log_path, f"\n[END] status={tr.status} duration={tr.duration_sec:.2f}s\n")

    return tr
