"""図版を生成する。数値は experiment/summary.json から読む。light: <name>.svg / dark: <name>.dark.svg（viewBoxは同一）。"""
import json
from pathlib import Path
from xml.sax.saxutils import escape

OUT = Path(__file__).parent
S = json.loads((OUT / "experiment" / "summary.json").read_text())
THEMES = {
    "": dict(text="#1f2937", sub="#4b5563", line="#9ca3af", card="#f3f4f6", acc="#1d4ed8", warn="#b45309", bad="#b91c1c", ok="#15803d", vio="#6d28d9", barbg="#e5e7eb"),
    ".dark": dict(text="#f3f4f6", sub="#d1d5db", line="#6b7280", card="#1f2937", acc="#93c5fd", warn="#fbbf24", bad="#fca5a5", ok="#86efac", vio="#c4b5fd", barbg="#374151"),
}
FONT = "sans-serif"
DEFS = ('<defs><marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">'
        '<path d="M0,0 L10,5 L0,10 z" fill="{line}"/></marker></defs>')


def t(x, y, s, size=15, fill="text", weight="normal", anchor="start"):
    return f'<text x="{x}" y="{y}" font-family="{FONT}" font-size="{size}" font-weight="{weight}" fill="{{{fill}}}" text-anchor="{anchor}">{escape(s)}</text>'


def box(x, y, w, h, stroke="line", fill="card", sw=2, rx=12, dash=None, op=1):
    d = f' stroke-dasharray="{dash}"' if dash else ""
    return f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{rx}" fill="{{{fill}}}" fill-opacity="{op}" stroke="{{{stroke}}}" stroke-width="{sw}"{d}/>'


def arrow(x1, y1, x2, y2):
    return f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="{{line}}" stroke-width="2.5" marker-end="url(#ah)"/>'


def svg(w, h, title, body):
    return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" role="img" aria-label="{escape(title)}">'
            f'<title>{escape(title)}</title>' + DEFS + "".join(body) + "</svg>\n")


def fig_cycle():
    w, h = 800, 384
    b = [t(24, 34, "1周＝モデルが決めて、プログラムが実行して、結果を戻す", 20, weight="bold"),
         t(24, 58, "決めるのはAI、動かすのはプログラム（ハーネス）。この往復を、ツールを呼ばなくなるまで繰り返す", 14, "sub")]
    b += [box(40, 100, 270, 120, "acc"), t(175, 134, "モデル（AI）", 18, "acc", "bold", "middle"),
          t(175, 162, "履歴を読んで、次にやることを決める", 14, "text", "normal", "middle"),
          t(175, 186, "ツールを呼ぶ？ もう答える？", 14, "text", "normal", "middle")]
    b += [box(490, 100, 270, 120, "warn"), t(625, 134, "ハーネス（プログラム）", 18, "warn", "bold", "middle"),
          t(625, 162, "ツールを実際に実行する", 14, "text", "normal", "middle"),
          t(625, 186, "許可・上限・失敗の扱いも担当", 14, "text", "normal", "middle")]
    b += [arrow(312, 138, 488, 138), t(400, 128, "tool_use", 14, "text", "bold", "middle"),
          t(400, 160, "これ実行して", 13, "sub", "normal", "middle"),
          arrow(488, 188, 312, 188), t(400, 182, "tool_result", 14, "text", "bold", "middle"),
          t(400, 210, "結果です（履歴に追記）", 13, "sub", "normal", "middle")]
    b += [arrow(175, 222, 175, 282), t(190, 256, "ツールを呼ばず、テキストだけ返した", 14, "text", "bold")]
    b += [box(40, 288, 400, 74, "ok"), t(240, 318, "ループの出口（end_turn）", 17, "ok", "bold", "middle"),
          t(240, 344, "ここで「完了」。中身が正しいかは、まだ誰も見ていない", 13, "sub", "normal", "middle")]
    b += [box(470, 288, 290, 74, "bad", dash="6 5"), t(615, 318, "強制的な出口", 17, "bad", "bold", "middle"),
          t(615, 344, "上限（回数・費用）・人の中断・エラー", 13, "sub", "normal", "middle")]
    return svg(w, h, "エージェントループの1周。モデルがツール呼び出しを出し、ハーネスが実行して結果を返す。モデルがツールを呼ばずテキストだけ返したら出口。回数や費用の上限などの強制的な出口もある", b)


def fig_log():
    w, h = 800, 450
    lap = S["lap-1"]
    toks = lap["input_tokens_per_call"]
    rows = [("Bash：python3 test_cart.py", "FAIL 2つ・PASS 1つ"),
            ("Read × 2（テストと本体を同時に）", "2ファイルの中身"),
            ("Edit：cart.py の掛け算を直す", "更新しました"),
            ("Bash：python3 test_cart.py", "PASS 3つ"),
            ("ツールなし。テキストで「完了しました」", "→ 出口")]
    b = [t(24, 34, "実ログの1本：5回モデルを呼んで、5周で終わった", 20, weight="bold"),
         t(24, 58, "テストが落ちているカートのバグを直させた実行（Claude Code・Haiku 4.5、5本のうち1本）", 14, "sub"),
         t(24, 96, "周", 13, "sub"), t(60, 96, "モデルがしたこと → 返ってきたもの", 13, "sub"),
         t(470, 96, "その周にモデルへ送った入力（トークン）", 13, "sub")]
    x0, bw, scale = 470, 260, 25000
    for i, (a, r) in enumerate(rows):
        y = 112 + i * 62
        out = i == 4
        b.append(box(16, y, 768, 52, "ok" if out else "line", sw=2, rx=10))
        b.append(t(36, y + 33, str(i + 1), 18, "acc", "bold", "middle"))
        b.append(t(60, y + 24, a, 15, "text", "bold"))
        b.append(t(60, y + 44, r, 13, "sub"))
        base = min(toks[i], toks[0])
        b.append(f'<rect x="{x0}" y="{y+14}" width="{bw*base/scale:.0f}" height="24" rx="4" fill="{{line}}" fill-opacity="0.55"/>')
        if toks[i] > toks[0]:
            b.append(f'<rect x="{x0 + bw*toks[0]/scale:.0f}" y="{y+14}" width="{bw*(toks[i]-toks[0])/scale:.0f}" height="24" rx="2" fill="{{acc}}"/>')
        b.append(t(x0 + bw * toks[i] / scale + 8, y + 32, f"{toks[i]:,}", 14, "text", "bold"))
    b.append(t(24, 440, f"灰＝初回と同じ分（Claude Code側の指示やツール説明が大半と考えられる）／青＝初回からの増加分（最後の周で +{toks[-1]-toks[0]:,}）", 13, "sub"))
    return svg(w, h, "実ログの1本。5回のモデル呼び出しで、テスト実行、2ファイルの読み込み、編集、再テスト、完了報告の順に進み、入力トークンは21,235から24,006へ増えた", b)


def fig_three():
    w, h = 800, 520
    b = [t(24, 34, "「ループ」と言ったとき、3つの別物が混ざっている", 20, weight="bold"),
         t(24, 58, "内側のループは、外側のループの中で何度も回る。誰が次の1周を始めるかが違う", 14, "sub")]
    b += [box(24, 84, 752, 424, "warn", op=0.35, dash="8 6"),
          t(48, 114, "③ 外側のループ", 18, "warn", "bold"),
          t(190, 114, "人・スケジュール・シェルが、新しい依頼やセッションを始める", 14, "text"),
          t(48, 138, "例：while文でclaude -pを呼び直す（Ralph原典）／ /loop／ 定期実行／「ループエンジニアリング」", 13, "sub")]
    b += [box(56, 164, 688, 320, "vio", op=0.35),
          t(80, 194, "② 目標型のループ", 18, "vio", "bold"),
          t(240, 194, "評価役が「まだ」と言う限り、同じ会話で続く", 14, "text"),
          t(80, 218, "例：/goal ／ Ralph公式plugin（同じ会話で、合言葉が出るまで）", 13, "sub")]
    b += [box(88, 244, 624, 216, "acc", op=0.45),
          t(112, 274, "① 内側のループ（エージェントループ）", 18, "acc", "bold"),
          t(112, 300, "1回の依頼の中で、ツールを呼ばなくなるまで回る", 14, "text"),
          t(112, 330, "モデルが決める → ハーネスが実行 → 結果を履歴へ → モデルが決める …", 14, "text"),
          t(112, 362, "次の1周を始めるのは：モデル（ツールを呼びたいと言う間）", 14, "text", "bold"),
          t(112, 392, "出口：ツールを呼ばないテキスト／回数・費用の上限", 14, "sub"),
          t(112, 420, "状態の置き場：会話の履歴（毎周まるごと再送）", 14, "sub"),
          t(112, 448, "この記事の主役。docsが「agent loop」と呼ぶのはこれ", 14, "sub")]
    return svg(w, h, "3つのループの入れ子。内側のループはモデルとツールの往復で、その外に評価役が続けさせる目標型のループがあり、さらに外に人やスケジュールやシェルが回す外側のループがある", b)


def fig_exp():
    w, h = 800, 512
    def stats(prefix):
        ks = [k for k in S if k.startswith(prefix)]
        return [S[k] for k in ks]
    plain, goal = stats("plain"), stats("goal")
    outer = [S[f"outer-{i}"] for i in (1, 2, 3)]
    pc = [x["cost_usd"] for x in plain]; gc = [x["cost_usd"] for x in goal]; oc = [x["cost_usd"] for x in outer]
    ok = lambda xs: sum("ALL OK" in x["final_state"] for x in xs)
    rng = lambda xs: f"${min(xs):.2f}〜${max(xs):.2f}"
    b = [t(24, 34, "同じ課題を、3つのループで回してみた", 20, weight="bold"),
         t(24, 58, "5つの関数のうち4つが壊れているコードを、check.sh が ALL OK になるまで直す（各3回）", 14, "sub")]
    bx0, bw, gap = 190, 100, 36

    def row(y, label, sub, n, sep_label, sep_col, end_ok, line1, line2):
        r = [t(24, y + 26, label, 17, "text", "bold"), t(24, y + 46, sub, 12, "sub")]
        x = bx0
        for i in range(n):
            r.append(box(x, y, bw, 48, "acc", rx=8, op=0.6))
            r.append(t(x + bw / 2, y + 22, "内側のループ", 12, "text", "normal", "middle"))
            r.append(t(x + bw / 2, y + 39, f"f{i+1}を直す", 13, "text", "bold", "middle"))
            x += bw
            if i < n - 1:
                r.append(arrow(x + 2, y + 24, x + gap - 4, y + 24))
                r.append(t(x + gap / 2, y - 6, sep_label, 11, sep_col, "bold", "middle"))
                x += gap
        if end_ok:
            r.append(t(x + 22, y + 34, "✓", 30, "ok", "bold", "middle"))
        else:
            r.append(t(x + 22, y + 34, "✕", 30, "bad", "bold", "middle"))
        r.append(t(bx0, y + 76, line1, 14, "text", "bold"))
        r.append(t(bx0, y + 98, line2, 13, "sub"))
        return r

    b += row(120, "ふつうに頼む", "① だけ", 1, "", "sub", False,
             f"成功 {ok(plain)}/3　1つ直したら報告して止まり、残りは直らない",
             f"モデル呼び出し {min(x['model_calls'] for x in plain)}〜{max(x['model_calls'] for x in plain)}回・{rng(pc)}")
    b += row(260, "/goal", "① ＋ ②", 4, "評価役「まだ」", "vio", True,
             f"成功 {ok(goal)}/3　評価役の「まだ」が {min(x['stop_hook_feedbacks'] for x in goal)}〜{max(x['stop_hook_feedbacks'] for x in goal)}回出て、同じ会話が続いた",
             f"モデル呼び出し {min(x['model_calls'] for x in goal)}〜{max(x['model_calls'] for x in goal)}回・{rng(gc)}")
    b += row(400, "シェルで呼び直す", "① ＋ ③", 4, "新セッション", "warn", True,
             f"成功 {ok(outer)}/3　{min(x['rounds'] for x in outer)}〜{max(x['rounds'] for x in outer)}周、毎周ちがうセッション（上限は8周）",
             f"モデル呼び出し計 {min(x['model_calls'] for x in outer)}〜{max(x['model_calls'] for x in outer)}回・{rng(oc)}")
    return svg(w, h, "同じ課題を3方式で回した結果。ふつうに頼むと1関数だけ直して止まり0/3。/goalは評価役が続けさせて3/3。シェルで呼び直す方式も3/3", b)


FIGS = {"fig01-cycle": fig_cycle, "fig02-log": fig_log, "fig03-three": fig_three, "fig04-experiment": fig_exp}
for name, fn in FIGS.items():
    raw = fn()
    for suffix, th in THEMES.items():
        s = raw
        for k, v in th.items():
            s = s.replace("{" + k + "}", v)
        (OUT / f"{name}{suffix}.svg").write_text(s)
print("ok")
