#!/usr/bin/env python3
"""仕様書あり(spec) / 計画だけ(plan) / いきなり実装(direct) を同じ依頼で回す。

使い方: python3 run_experiment.py <fix|feature> <direct|plan|spec> <rep> [--runs-root /tmp/sdd-runs]
各試行は /tmp/sdd-runs/<task>-<cond>-<rep>/ に baseをコピーして実行し、
ログと採点を results/<task>-<cond>-<rep>/ に残す。
"""
import argparse, json, os, shutil, subprocess, sys, time, re
from pathlib import Path

HERE = Path(__file__).resolve().parent
MODEL = "claude-sonnet-5-5"
SPEC_KIT = "git+https://github.com/github/spec-kit@v1.2.0"
ENV = dict(os.environ, CLAUDE_CODE_DISABLE_AUTO_MEMORY="1")
TAIL = "\n\n実装して、既存のテストも通る状態にしてください。"


def sh(cmd, cwd, **kw):
    return subprocess.run(cmd, cwd=cwd, text=True, capture_output=True, **kw)


def claude(cwd, prompt, log, mode="bypassPermissions", resume=None):
    cmd = ["claude", "-p", prompt, "--model", MODEL, "--output-format", "stream-json",
           "--verbose", "--permission-mode", mode, "--setting-sources", "project"]
    if resume:
        cmd += ["--resume", resume]
    t0 = time.time()
    p = subprocess.run(cmd, cwd=cwd, env=ENV, text=True, capture_output=True, stdin=subprocess.DEVNULL)
    wall = time.time() - t0
    log.write_text(log.read_text() + p.stdout if log.exists() else p.stdout)
    res = {}
    for line in p.stdout.splitlines():
        try:
            ev = json.loads(line)
        except Exception:
            continue
        if ev.get("type") == "result":
            res = ev
    return dict(session=res.get("session_id"), cost=res.get("total_cost_usd", 0.0),
                turns=res.get("num_turns", 0), api_ms=res.get("duration_api_ms", 0),
                wall_s=round(wall, 1), is_error=res.get("is_error"), text=res.get("result", ""),
                stderr=p.stderr[-500:])


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("task"); ap.add_argument("cond"); ap.add_argument("rep")
    ap.add_argument("--runs-root", default="/tmp/sdd-runs")
    a = ap.parse_args()
    rid = f"{a.task}-{a.cond}-{a.rep}"
    work = Path(a.runs_root) / rid
    out = HERE / "results" / rid
    if work.exists():
        shutil.rmtree(work)
    shutil.copytree(HERE / "base", work)
    out.mkdir(parents=True, exist_ok=True)
    for f in out.iterdir():
        f.unlink() if f.is_file() else shutil.rmtree(f)
    sh(["git", "init", "-q"], work)
    sh(["git", "-c", "user.name=x", "-c", "user.email=x@x", "add", "-A"], work)
    sh(["git", "-c", "user.name=x", "-c", "user.email=x@x", "commit", "-qm", "base"], work)
    request = (HERE / "tasks" / f"{a.task}.md").read_text().strip()
    log = out / "stream.jsonl"
    phases = []

    if a.cond == "spec":
        r = sh(["uvx", "--from", SPEC_KIT, "specify", "init", "--here", "--force",
                "--non-interactive", "--integration", "claude"], work)
        (out / "specify-init.txt").write_text(r.stdout[-3000:] + r.stderr[-1000:])
        sh(["git", "-c", "user.name=x", "-c", "user.email=x@x", "add", "-A"], work)
        sh(["git", "-c", "user.name=x", "-c", "user.email=x@x", "commit", "-qm", "spec-kit scaffold"], work)

    base_sha = sh(["git", "rev-parse", "HEAD"], work).stdout.strip()

    if a.cond == "direct":
        phases.append(("implement", claude(work, request + TAIL, log)))
    elif a.cond == "look":
        phases.append(("implement", claude(work, request + "\n\n直す前に、関係するコードを読んで、影響範囲を確認してください。" + TAIL, log)))
    elif a.cond == "plan":
        p1 = claude(work, request + "\n\nまず実装計画を立ててください。まだコードは書かないでください。", log, mode="plan")
        phases.append(("plan", p1))
        (out / "plan.txt").write_text(p1["text"])
        phases.append(("implement", claude(work, "計画どおりに実装してください。既存のテストも通る状態にしてください。", log, resume=p1["session"])))
    elif a.cond == "spec":
        sid = None
        for name, prompt in [("specify", "/speckit-specify " + request),
                             ("plan", "/speckit-plan"),
                             ("tasks", "/speckit-tasks"),
                             ("implement", "/speckit-implement")]:
            r = claude(work, prompt, log, resume=sid)
            sid = r["session"] or sid
            phases.append((name, r))
    # 採点
    hidden = f"test_{a.task}.py"
    shutil.copy(HERE / "hidden" / hidden, work / "tests" / "_hidden.py")
    rh = sh([sys.executable, "-m", "unittest", "tests._hidden", "-v"], work)
    (out / "hidden-output.txt").write_text(rh.stdout + rh.stderr)
    (work / "tests" / "_hidden.py").unlink()
    ex = sh([sys.executable, "-m", "unittest", "discover", "-s", "tests", "-t", "."], work)
    existing_ok = ex.returncode == 0
    hres = {}
    for m in re.finditer(r"test_([CEF]\d)\w* \(.*?\) \.\.\. (ok|FAIL|ERROR)", rh.stderr + rh.stdout):
        hres[m.group(1)] = m.group(2) == "ok"
    # 差分
    sh(["git", "-c", "user.name=x", "-c", "user.email=x@x", "add", "-A"], work)
    stat = sh(["git", "diff", "--cached", "--numstat", base_sha], work).stdout.splitlines()
    src_add = src_del = docs_add = 0; docs_files = []; src_files = []
    for line in stat:
        ad, de, path = line.split("\t", 2)
        if ad == "-":
            continue
        ad, de = int(ad), int(de)
        if path.endswith(".py") and (path.startswith("taskboard/") or path.startswith("tests/")):
            src_add += ad; src_del += de; src_files.append(path)
        elif path.startswith(".specify/") or path.startswith(".claude/") or "__pycache__" in path:
            continue
        else:
            docs_add += ad; docs_files.append(path)
    # 作業フォルダ外へのアクセス検出（Read/Write/Editのfile_path と、Bashの /Users 配下パス）
    outside = []
    real_work = str(work.resolve())
    for line in log.read_text().splitlines():
        try:
            ev = json.loads(line)
        except Exception:
            continue
        if ev.get("type") != "assistant":
            continue
        for c in ev["message"].get("content", []):
            if c.get("type") != "tool_use":
                continue
            inp = c["input"]
            if c["name"] in ("Read", "Write", "Edit", "NotebookEdit", "Glob", "Grep"):
                paths = [v for k, v in inp.items() if k in ("file_path", "path") and isinstance(v, str)]
            elif c["name"] == "Bash":
                paths = re.findall(r"/Users/[\w./-]+", inp.get("command", ""))
            else:
                paths = []
            for pth in paths:
                if pth.startswith(("<Claudeの設定ディレクトリ>", "<HOME>/.claude")) and "/plans/" in pth:
                    continue  # Claude Codeが自分で置く計画ファイル
                if not (pth.startswith(real_work) or pth.startswith(str(work))):
                    outside.append((c["name"], pth))
    summary = dict(id=rid, task=a.task, cond=a.cond, rep=a.rep, hidden=hres,
                   existing_tests_ok=existing_ok,
                   cost=round(max(p["cost"] for _, p in phases), 4),  # resumeすると累積値になるので最大=最終
                   wall_s=round(sum(p["wall_s"] for _, p in phases), 1),
                   turns=sum(p["turns"] for _, p in phases),
                   api_s=round(max(p["api_ms"] for _, p in phases) / 1000, 1),
                   src_added=src_add, src_deleted=src_del, src_files=src_files,
                   docs_added=docs_add, docs_files=docs_files,
                   outside_access=outside[:10],
                   phases=[{k: v for k, v in p.items() if k != "text"} | {"name": n} for n, p in phases])
    (out / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2))
    sh(["git", "diff", "--cached", base_sha, "--", "taskboard", "tests"], work)
    (out / "src.diff").write_text(sh(["git", "diff", "--cached", base_sha, "--", "taskboard", "tests"], work).stdout)
    # 仕様書類・計画書類を保存（spec-kitの足場は除く）
    keep = out / "docs"
    for pth in docs_files:
        dst = keep / pth
        dst.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy(work / pth, dst)
    print(json.dumps({k: summary[k] for k in ("id", "hidden", "existing_tests_ok", "cost", "wall_s", "turns", "src_added", "docs_added")}, ensure_ascii=False))


if __name__ == "__main__":
    main()
