"""
=============================================================================
第 15 讲 · 代码 06（可选）：让真实的 Claude 修 bug——Agentless 式的一次调用
=============================================================================

对应讲义：lectures/lecture15_coding_agents.md §7

【这个脚本要回答的问题】
    代码 02–04 里"模型的判断"是模拟的。换成真实的模型呢？为了让调用次数少、结果好判，这里用 Agentless 的形状：
        定位用代码做（从 issue 里的函数顺着调用找到相关的函数和常量，真实解析）；
        修复交给 Claude：给它 issue 和这些代码（带行号），让它输出一个 SEARCH/REPLACE 格式的编辑；
        判分照 SWE-bench：打上补丁，跑全部测试（含隐藏的 FAIL_TO_PASS），再做差分检查。
    默认取 8 个任务（每个文件至少一个），每个任务 1 次调用。

【需要什么】pip install anthropic；设置 ANTHROPIC_API_KEY（或 `ant auth login`）。**会花钱**：8 次调用。
    没有 SDK 或凭证时打印说明后正常退出。
【运行】
    python3 06_real_llm.py           # 默认 claude-opus-5-5，effort=low
    python3 06_real_llm.py --toy     # 不联网：用 agent_lab.agentless 的模拟模型（检查脚本本身）
"""

import argparse
import os
import re
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "lecture01"))
from agent_lab import agentless, defs, related  # noqa: E402
from console import print_header, print_observations, print_table  # noqa: E402
from real_llm import DEFAULT_MODEL, ClaudeLM, sdk_available  # noqa: E402
from repo_lab import ORIGINAL, SRC_FILES, evaluate_patch, load_tasks  # noqa: E402

SYSTEM = ("You are fixing a bug in a small Python package. You will get a bug report and the relevant code with line numbers. "
          "Reply with exactly one edit in this format and nothing else:\n"
          "<<<<<<< SEARCH\n<exact existing lines, without line numbers>\n=======\n<replacement lines>\n>>>>>>> REPLACE")


def context_for(task, sources):
    """把 issue 里的函数和它用到的函数、常量的代码拼起来（带行号），按文件分组。"""
    d = defs(sources)
    parts = []
    for nm in related(task.entry, sources):
        if nm not in d:
            continue
        rel, lo, hi = d[nm]
        lines = sources[rel].split("\n")
        parts.append(f"### {rel} (lines {lo}-{hi})\n" + "\n".join(f"{i}: {lines[i - 1]}" for i in range(lo, hi + 1)))
    return "\n\n".join(parts)


def apply_edit(text, sources):
    """解析 SEARCH/REPLACE，在源文件里找到 SEARCH 的原文并替换。【返回】(新的源文件, 说明)"""
    m = re.search(r"<<<<<<< SEARCH\n(.*?)\n=======\n(.*?)\n>>>>>>> REPLACE", text, flags=re.S)
    if not m:
        return None, "格式不对"
    search, replace = m.group(1), m.group(2)
    hits = [rel for rel in SRC_FILES if search in sources[rel]]
    if len(hits) != 1:
        return None, f"SEARCH 在 {len(hits)} 个文件里出现"
    rel = hits[0]
    new = dict(sources, **{rel: sources[rel].replace(search, replace, 1)})
    try:
        compile(new[rel], rel, "exec")
    except SyntaxError as e:
        return None, f"语法错误：{e.msg}"
    return new, "已应用"


def pick(tasks, k=8):
    """每个文件至少挑一个任务，再按顺序补满 k 个。"""
    out, seen = [], set()
    for t in tasks:
        if t.mutation.file not in seen:
            out.append(t)
            seen.add(t.mutation.file)
    for t in tasks:
        if len(out) >= k:
            break
        if t not in out and t.mutation.func != t.entry:
            out.append(t)
    return out[:k]


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--model", default=DEFAULT_MODEL)
    ap.add_argument("--effort", default="low")
    ap.add_argument("--toy", action="store_true")
    args = ap.parse_args()
    tasks, _ = load_tasks()
    chosen = pick(tasks)

    if not args.toy:
        ok, why = sdk_available()
        if not ok:
            print(why)
            return
        lm = ClaudeLM(model=args.model, effort=args.effort, max_tokens=4000, stop_sequences=())
        name = lm.name
    else:
        name = "toy（agent_lab.agentless 的模拟模型）"

    rows, n_res, n_diff = [], 0, 0
    try:
        for t in chosen:
            m = t.mutation
            sources = dict(ORIGINAL, **{m.file: m.apply(ORIGINAL[m.file])})
            if args.toy:
                info, final = agentless(t)
                note = info["chosen"]
            else:
                prompt = f"Bug report:\n{t.issue}\n\nRelevant code:\n{context_for(t, sources)}"
                reply = lm([{"role": "system", "content": SYSTEM}, {"role": "user", "content": prompt}])
                final, note = apply_edit(reply, sources)
                if final is None:
                    final = sources
            ev = evaluate_patch(t, final)
            n_res += ev["resolved"]
            n_diff += ev["resolved"] and ev["diff_ok"] is not False
            rows.append([t.tid, t.issue[:60] + ("…" if len(t.issue) > 60 else ""), note, "是" if ev["resolved"] else "否",
                         {True: "一致", False: "不一致", None: "-"}[ev["diff_ok"]]])
    except Exception as e:  # noqa: BLE001
        if not args.toy and type(e).__name__ in ("AuthenticationError", "APIConnectionError"):
            print("凭证无效 / 连不上 API：设置 ANTHROPIC_API_KEY 或运行 ant auth login，并检查网络")
            return
        raise
    print_header(f"模型 {name}：{len(chosen)} 个任务")
    print_table(["任务", "issue", "编辑", "resolved", "差分检查"], rows, aligns=["<", "<", "<", "<", "<"])
    print(f"\nresolved {n_res}/{len(chosen)}；其中差分检查也一致的 {n_diff}")
    print_observations([
        "没修好的那几个：是定位给的代码里根本没有 bug（看 bug 在哪个函数），还是模型改错了地方、改错了方式？",
        "模型有没有'改得比需要的多'（例如顺手重写了整个函数）？这类补丁更容易弄坏 PASS_TO_PASS。",
        "8 个任务太少，下不了统计结论；它的用途是读一读真实模型的补丁长什么样。",
    ])


if __name__ == "__main__":
    main()
