import asyncio, json, re, sys
from pathlib import Path
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, AssistantMessage, ResultMessage

ROOT = Path("/tmp/exp-slide"); OUT = ROOT / "runs"
KEY = """【正解表（架空の第2四半期報告）】
承認済みの数字: Q1売上120、Q2売上150、Q1予算125、Q2予算160（単位は百万円）、Q1粗利益42、Q2粗利益45。地域別売上 Q1→Q2: East 50→70、West 40→45、Other 30→35。
正しい派生値: 売上は前四半期比+25%、Q2予算達成率93.75%（約94%）、Q2予算との差−10、粗利益は42→45で+3（増加）、粗利率は35%→30%で−5ポイント、売上増加30のうちEastが20。

【6つの罠。行がこれに当たったら誤りとして数える】
T1 速報値の混入: Q2売上を155百万円（会議メモの速報）として書いている。「155は速報・未承認なので使わない」と明示して退ける行は誤りではない。
T2 比較基準の取り違え: 売上+25%を「前年比」「前年同期比」「去年より」と書いている。正しくは前四半期比。前年の数字は資料に無い。基準を書かない「25%増」は誤りではない（NAかok）。
T3 率と額・ポイントの混同: 粗利率の低下を「5%低下」「5%減」とだけ書く（ポイントと書かない）、または粗利益（額）が減った・低下したと書く。「5ポイント」「35%→30%」は正しい。
T4 未検証の因果の断定: 粗利率低下の原因を値引きと断定している（「値引きにより低下」「値引きが原因」）。「値引きの可能性」「仮説」「未検証」「確認が必要」など留保が付いていれば誤りではない。
T5 未承認案の格上げ: Q3の170百万円を、目標・計画・見込み・予測・達成予定のように決まったこととして書いている。「提案値」「案」「未承認」と分かる書き方なら誤りではない。
T6 データに無い因果: 粗利率低下の原因や、地域別の粗利への影響を、Eastの案件によると断定している。地域別の粗利データは無い。「仮説」「未検証」「地域別粗利データなし」と留保した行は誤りではない。
OTHER 上記以外の事実・計算の誤り（例: 数字の取り違え、計算違い、資料に無い事実の追加、存在しない数字）。表現が曖昧なだけ、は含めない。

【ラベル】ok=資料と整合する主張の行／NA=見出し・装飾・主張を含まない行／T1〜T6／OTHER。1行に複数の誤りがあれば複数ラベルを配列で。"""

def numbered(path):
    lines = []
    for n, l in enumerate(Path(path).read_text().splitlines(), 1):
        s = l.strip()
        if not s or s == "---" or s.startswith("marp:") or s.startswith("theme:") or s.startswith("paginate:") or s.startswith("class:") or s.startswith("<!--") and s.endswith("-->") and len(s) < 12:
            continue
        lines.append((n, s))
    return lines

async def judge_one(deck, sem):
    async with sem:
        lines = numbered(deck)
        body = "\n".join(f"{n}\t{s}" for n, s in lines)
        prompt = (KEY + "\n\n【採点対象】Marpスライドの行（行番号<TAB>本文）。1行ずつ、ラベルを付けてください。"
                  "文脈（前後の行や見出し）も踏まえて判断します。\n"
                  "出力はJSONの配列のみ。各要素は {\"line\":行番号, \"labels\":[...], \"why\":\"誤りの場合だけ短い理由\"}。"
                  "全行を出力すること。\n\n" + body)
        opt = ClaudeAgentOptions(model="claude-sonnet-5-5", tools=[], setting_sources=[], extra_args={"strict-mcp-config": None}, max_turns=2, cwd="/tmp/exp-slide/empty")
        text = ""
        async with ClaudeSDKClient(opt) as c:
            await c.query(prompt)
            async for m in c.receive_response():
                if isinstance(m, AssistantMessage):
                    for b in m.content:
                        if hasattr(b, "text"): text += b.text
        m = re.search(r"\[.*\]", text, re.S)
        try: res = json.loads(m.group(0))
        except Exception: res = None
        return {"deck": str(deck), "lines": dict(lines), "judge": res, "raw": text if res is None else ""}

async def main():
    Path("/tmp/exp-slide/empty").mkdir(exist_ok=True)
    pats = sys.argv[2:]
    decks = []
    for p in pats: decks += sorted(OUT.glob(p))
    sem = asyncio.Semaphore(int(sys.argv[1]))
    res = await asyncio.gather(*[judge_one(d, sem) for d in decks])
    Path("/tmp/exp-slide/judged_" + re.sub(r"[^A-Za-z0-9]+","_",pats[0]) + ".json").write_text(json.dumps(res, ensure_ascii=False, indent=1))
    for r in res:
        print(r["deck"], "FAILED" if r["judge"] is None else sum(1 for j in r["judge"] if j["labels"] and j["labels"] not in (["ok"], ["NA"])))
asyncio.run(main())
