"""考察記事のサムネイル（問いを文字で組んだ形）を HTML で書き出す。

考察記事は Gemini の絵（4コマ）ではなく、記事の問いを文字で組んだサムネイルにする（2026-10-26 から。採用は 3-B「3段階」）。
問いと強く見せる一語の正は tools/deep_dive_thumbs.json（記事1本＝1項目）。
  question : 記事の問い。縦書きで改行したい所に「/」。1行9字まで（数字の並びは1字、英字は1字ずつ数える）
  key      : 強く見せる一語。問いの1行の中にある語
  layout   : 組み（0 縦・右寄せ／1 横・左下／2 縦・左寄せ／3 横・右上）。記事ごとに固定し、新しい記事が増えても既存の見え方は変えない

使い方:
  python3 tools/deep_dive_thumb.py                          # 全部の考察記事のサムネイルを public/thumbs/LOG{N}.html に書き出す
  python3 tools/deep_dive_thumb.py --add <LOG番号> "<問い>" "<一語>"   # 新しい考察記事を足して書き出す（組みは前の記事の続きを自動で決める）
  python3 tools/deep_dive_thumb.py --batch <JSON> [--plan p3b]         # 確認用：案ごとに書き出す（出力先は JSON の "out"/{案}/）
書き出したあと、SNS・検索結果用の画像は python3 tools/render_og.py LOG{N}（クラウドの作業環境で）。

案（組み方）。確認用ページ /preview/deep-dive-thumbs で比べたもの
  plain : 問いを縦書きで1つ置くだけ（最初の試作）
  p1    : 一語を大きく。問いの中の一語を絵のように大きく置き、問いは小さく添える
  p2    : 黒い面で分ける。黒い面に一語を白で抜き、紙の面に問いを置く
  p3    : 文の強弱。問いの文をそのまま組み、一語だけ大きく、残りは小さくする
  p3a   : 案3の強弱を控えめに（一語は今の半分ほど、文は大きめ）
  p3b   : 案3を3段階に（一語・問いの結び・残り）← 採用
  p3c   : 案3を太さだけで差をつける（大きさは全部同じ、一語は極太・残りは極細）

色は紙と墨の2色だけ。書体は明朝（考えのページ /thinking とバナー ThinkingBanner にそろえる）。
"""
import html
import json
import re
import sys
from pathlib import Path

PAPER = "#f5f3ee"
INK = "#22201c"
MAX_CHARS = 9  # 英字を縦に1字ずつ立てたので「フィジカルAIは、」が9字になる
PARTICLES = set("のにはをでがとも")  # 一語の後ろで改行してよい助詞
PLANS = ["plain", "p1", "p2", "p3", "p3a", "p3b", "p3c"]

HEAD = """<!doctype html>
<html lang="ja" class="wait">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>LOG{log} thumbnail</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@200;300;400;700;900&display=block" rel="stylesheet">
<style>
  * {{ box-sizing: border-box; margin: 0; padding: 0; }}
  html, body {{ background: {paper}; }}
  .wait .wrap > * {{ visibility: hidden; }}
  .wrap {{ width: 100%; aspect-ratio: 16 / 9; container-type: inline-size; position: relative; overflow: hidden; background: {paper}; color: {ink};
    font-family: "Noto Serif JP", "Hiragino Mincho ProN", "Yu Mincho", serif; }}
  .v {{ writing-mode: vertical-rl; white-space: nowrap; }}
  .h {{ white-space: nowrap; }}
  .tcy {{ text-combine-upright: all; letter-spacing: 0; }}
  .up {{ text-orientation: upright; }}
  .abs {{ position: absolute; }}
  .blk {{ position: absolute; background: {ink}; color: {paper}; }}
  .key {{ font-weight: 900; line-height: 1; letter-spacing: 0; }}
  .sm {{ font-weight: 400; letter-spacing: .06em; }}
  .b {{ font-weight: 900; }}
{css}
</style>
</head>
<body>
<div class="wrap">{body}</div>
<script>
  // 書体がすべて届いてから組み直して出す。日本語の書体は文字ごとに分かれて届くため、途中は別の明朝で並び、
  // 縦書きの数字（縦中横）の幅が最初の書体のまま残って、読み込むたびに位置が変わっていた（2026-10-26）
  (function () {{
    var done = false;
    function show() {{
      if (done) return; done = true;
      var w = document.querySelector('.wrap');
      w.style.display = 'none'; void w.offsetHeight; w.style.display = '';
      document.documentElement.classList.remove('wait');
    }}
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(function () {{ requestAnimationFrame(show); }});
    else show();
    setTimeout(show, 4000);  // 書体が届かないときも4秒で出す
  }})();
</script>
</body>
</html>
"""


def vlen(s: str) -> float:
    """縦に並べたときの長さ（字数）。縦中横にする1〜3字の数字は1字に数え、英字は1字ずつ数える"""
    return len(re.sub(r"[0-9]{1,3}", "x", s))


def hlen(s: str) -> float:
    """横に並べたときの長さ（字数）。英数字は半分強の幅で数える"""
    return sum(0.6 if re.match(r"[A-Za-z0-9]", ch) else 1 for ch in s)


def esc(s: str, vertical: bool) -> str:
    s = html.escape(s)
    if vertical:
        # 英字（AI など）は1字の幅に収まらず列からはみ出すので、縦に1字ずつ立てて組む
        s = re.sub(r"([A-Za-z]+)", r'<span class="up">\1</span>', s)
        # 縦書きの中で、（英字の後に入れる。先に入れると、印の中の英字まで囲んでしまう）1〜3字の数字（4・70 など）は横に倒さず、横に並べて1字分に組む（縦中横）
        s = re.sub(r"(?<![0-9])([0-9]{1,3})(?![0-9])", r'<span class="tcy">\1</span>', s)
    return s


def lines_of(q: str) -> list[str]:
    return [l for l in q.split("/") if l]


def hlines(q: str) -> list[str]:
    """横書きのときの行。縦書き用の短い行を2つずつつなぐ"""
    ls = lines_of(q)
    return ["".join(ls[i:i + 2]) for i in range(0, len(ls), 2)]


def small_v(q: str) -> str:
    return "<br>".join(esc(l, True) for l in lines_of(q))


def small_h(q: str) -> str:
    return "<br>".join(esc(l, False) for l in hlines(q))


# ── plain：問いを縦書きで1つ ──────────────────────────────
def plain(it, n):
    css = "  .q { right: 10cqw; top: 6.5cqw; font-size: 5.3cqw; line-height: 1.75; letter-spacing: .08em; font-weight: 700; }"
    return css, f'<p class="abs v q">{small_v(it["question"])}</p>'


# ── p1：一語を大きく ─────────────────────────────────────
def p1(it, n):
    key, q = it["key"], it["question"]
    kv = round(min(22, 46 / vlen(key)), 2)   # 縦に置くときの大きさ（高さ 56.25cqw に収める）
    kh = round(min(26, 84 / hlen(key)), 2)   # 横に置くときの大きさ（幅 100cqw に収める）
    s = 3.3
    lay = n % 4
    if lay == 0:   # 一語を縦に右端、問いを縦にその左
        css = f"""  .k {{ right: 7cqw; top: 4.5cqw; font-size: {kv}cqw; }}
  .s {{ right: {7 + kv + 5}cqw; top: 6cqw; font-size: {s}cqw; line-height: 1.9; }}"""
        body = f'<p class="abs v key k">{esc(key, True)}</p><p class="abs v sm s">{small_v(q)}</p>'
    elif lay == 1:  # 一語を横に下へ大きく（少し下にはみ出す）、問いを横に左上
        css = f"""  .k {{ left: 4.5cqw; bottom: -{kh * 0.12:.2f}cqw; font-size: {kh}cqw; }}
  .s {{ left: 6cqw; top: 6cqw; font-size: {s}cqw; line-height: 1.8; }}"""
        body = f'<p class="abs h key k">{esc(key, False)}</p><p class="abs h sm s">{small_h(q)}</p>'
    elif lay == 2:  # 一語を横に右上、問いを縦に左
        css = f"""  .k {{ right: 5cqw; top: 5cqw; font-size: {min(kh, 22)}cqw; text-align: right; }}
  .s {{ left: 6cqw; top: 6cqw; font-size: {s}cqw; line-height: 1.9; }}"""
        body = f'<p class="abs h key k">{esc(key, False)}</p><p class="abs v sm s">{small_v(q)}</p>'
    else:           # 一語を縦に左、問いを横に右下
        css = f"""  .k {{ left: 6cqw; top: 4.5cqw; font-size: {kv}cqw; }}
  .s {{ right: 6cqw; bottom: 6cqw; font-size: {s}cqw; line-height: 1.8; text-align: right; }}"""
        body = f'<p class="abs v key k">{esc(key, True)}</p><p class="abs h sm s">{small_h(q)}</p>'
    return css, body


# ── p2：黒い面で分ける ───────────────────────────────────
def p2(it, n):
    key, q = it["key"], it["question"]
    s = 3.4
    lay = n % 4
    if lay == 0:   # 黒い面を右に縦長、一語は縦
        w = 36
        kv = round(min(19, 44 / vlen(key), w * 0.62), 2)
        css = f"""  .bk {{ right: 0; top: 0; bottom: 0; width: {w}cqw; display: flex; align-items: center; justify-content: center; }}
  .k {{ font-size: {kv}cqw; }}
  .s {{ right: {w + 7}cqw; top: 6cqw; font-size: {s}cqw; line-height: 1.9; }}"""
        body = f'<div class="blk bk"><p class="v key k">{esc(key, True)}</p></div><p class="abs v sm s">{small_v(q)}</p>'
    elif lay == 1:  # 黒い面を下に帯、一語は横
        hgt = 25
        kh = round(min(17, 84 / hlen(key)), 2)
        css = f"""  .bk {{ left: 0; right: 0; bottom: 0; height: {hgt}cqw; display: flex; align-items: center; padding-left: 6cqw; }}
  .k {{ font-size: {kh}cqw; }}
  .s {{ left: 6cqw; top: 6cqw; font-size: {s}cqw; line-height: 1.8; }}"""
        body = f'<div class="blk bk"><p class="h key k">{esc(key, False)}</p></div><p class="abs h sm s">{small_h(q)}</p>'
    elif lay == 2:  # 黒い面を左に、一語は縦。問いは右に縦
        w = 32
        kv = round(min(17, 44 / vlen(key), w * 0.6), 2)
        css = f"""  .bk {{ left: 0; top: 0; bottom: 0; width: {w}cqw; display: flex; align-items: center; justify-content: center; }}
  .k {{ font-size: {kv}cqw; }}
  .s {{ right: 8cqw; top: 6cqw; font-size: {s}cqw; line-height: 1.9; }}"""
        body = f'<div class="blk bk"><p class="v key k">{esc(key, True)}</p></div><p class="abs v sm s">{small_v(q)}</p>'
    else:           # 黒い面を右上に、一語は横。問いは左下に横
        w, hgt = 46, 30
        kh = round(min(15, (w - 8) / hlen(key)), 2)
        css = f"""  .bk {{ right: 0; top: 0; width: {w}cqw; height: {hgt}cqw; display: flex; align-items: center; justify-content: center; }}
  .k {{ font-size: {kh}cqw; }}
  .s {{ left: 6cqw; bottom: 6cqw; font-size: {s}cqw; line-height: 1.8; }}"""
        body = f'<div class="blk bk"><p class="h key k">{esc(key, False)}</p></div><p class="abs h sm s">{small_h(q)}</p>'
    return css, body


# ── p3：文の強弱 ─────────────────────────────────────────
# 強弱の付け方（大きさと太さ）だけを変えた型。組み（位置と縦・横）は4通りを順に回す
#   small: 小さい文字の大きさ（縦, 横）／ big: 一語の大きさの上限 ／ w: 太さ（一語, 残り）
#   mid: 問いの結びの行を中くらいにする（大きさ, 太さ）。None なら2段階
P3_STYLES = {
    "p3":  dict(small=(3.6, 3.8), big=(13, 15), w=(900, 400), mid=None),     # 今の案3：一語を大きく、残りを小さく
    "p3a": dict(small=(4.4, 4.6), big=(7.5, 8.5), w=(700, 400), mid=None),   # 差を控えめに：一語は今の半分ほど、文は大きめ
    "p3b": dict(small=(4.2, 4.4), big=(16, 18), w=(900, 300), mid=(6.2, 700), pad=4.5),  # 3段階：一語・問いの結び・残り。余白を詰め、文字を大きく
    "p3c": dict(small=(5.2, 5.0), big=(5.2, 5.0), w=(900, 200), mid=None),   # 太さだけで差：大きさは全部同じ、一語は極太・残りは極細
}


def p3_style(name):
    st = P3_STYLES[name]

    def render_p3(it, n):
        key, q = it["key"], it["question"]
        lay = n % 4
        vertical = lay in (0, 2)
        ls = lines_of(q) if vertical else hlines(q)
        if not any(key in l for l in ls):
            raise SystemExit(f"LOG{it['log']}: key「{key}」が問いの行の中で切れています。/ の位置を直してください")
        small = st["small"][0 if vertical else 1]
        L = vlen if vertical else hlen
        pad = st.get("pad")                    # 外側の余白（cqw）。None なら案3の標準
        room = (56.25 - pad * 2 if pad else 46) if vertical else (100 - pad * 2 if pad else 86)  # 1行が使える長さ
        k = 1.06 if vertical else 1.0
        cap = st["big"][0 if vertical else 1]

        def fit(ls):
            ki = next(i for i, l in enumerate(ls) if key in l)
            rest = ls[ki].replace(key, "", 1)
            return ki, round(min(cap, (room - L(rest) * small * k) / (L(key) * k)), 2)

        ki, big = fit(ls)
        if big < cap * 0.8:
            # 一語が上限の大きさまで届かないときは、一語の行を短くするため改行する（縦書き・横書きとも）
            # 切るのは「一語＋助詞1字（＋読点）」の後ろだけ。語の途中で切らない。残りが3字未満なら切らない
            # 例：フィジカルAIにあるものは何か。 → フィジカルAIに／あるものは何か。　4万円の教材と、 → 4万円の／教材と、
            a, b = ls[ki].split(key, 1)
            if b[:1] in PARTICLES:
                n = 2 if b[1:2] == "、" else 1
                head, tail = key + b[:n], b[n:]
                if len(tail) >= 3:
                    ls = ls[:ki] + [x for x in (a + head, tail) if x] + ls[ki + 1:]
                    ki, big = fit(ls)
        # 中くらいにする行：問いの結び（最後の行）。一語がその行にあれば最初の行
        mi = None
        if st["mid"] and len(ls) > 1:
            mi = len(ls) - 1 if ki != len(ls) - 1 else 0
        wb, ws = st["w"]
        # 大きい一語と中くらいの行は、その文字の大きさに合わせて行（縦書きでは列）が広がり、前後の行との間が開く。行の高さを詰める
        lb, lm = "line-height:1.12;", "line-height:1.25;"

        def line(i, l):
            if i == ki:
                a, b = l.split(key, 1)
                return f'{esc(a, vertical)}<span style="font-size:{big}cqw;font-weight:{wb};{lb}">{esc(key, vertical)}</span>{esc(b, vertical)}'
            if i == mi:
                ms = round(min(st["mid"][0], room / (L(l) * k)), 2)
                return f'<span style="font-size:{ms}cqw;font-weight:{st["mid"][1]};{lm}">{esc(l, vertical)}</span>'
            return esc(l, vertical)

        inner = "<br>".join(line(i, l) for i, l in enumerate(ls))
        pv, ph = (pad, pad) if pad else (5, 6)  # 縦の行の上の余白、横の行の端の余白
        pside = pad + 1.5 if pad else 7        # 縦書きの端。大きい英数字（70 など）が縦中横で幅を取るので、横書きより少し広く
        pos = {0: f"right: {pside}cqw; top: {pv}cqw;",                        # 縦、右寄せ
               1: f"left: {ph}cqw; bottom: {ph}cqw;",                         # 横、左下
               2: f"left: {pside}cqw; top: {pv}cqw;",                         # 縦、左寄せ
               3: f"right: {ph}cqw; top: {ph}cqw; text-align: right;"}[lay]  # 横、右上・右揃え
        css = f"  .t {{ {pos} font-size: {small}cqw; line-height: 1.55; letter-spacing: .04em; font-weight: {ws}; }}"
        if vertical:
            # 縦書きで大きさの違う文字が同じ列に並ぶと、小さい文字が列の右に寄る。列の中心にそろえる
            css += "\n  .t > span[style] { vertical-align: middle; }"
        return css, f'<p class="abs {"v" if vertical else "h"} t">{inner}</p>'

    return render_p3


RENDER = {"plain": plain, "p1": p1, "p2": p2, **{k: p3_style(k) for k in P3_STYLES}}


def render(it: dict, plan: str, n: int) -> str:
    longest = max(vlen(l) for l in lines_of(it["question"]))
    if longest > MAX_CHARS:
        raise SystemExit(f"LOG{it['log']}: 1行が{longest:g}字あります。8字までになるよう「/」で区切ってください")
    css, body = RENDER[plan](it, n)
    return HEAD.format(log=it["log"], paper=PAPER, ink=INK, css=css, body=body)


SITE = Path(__file__).resolve().parent.parent
DATA = SITE / "tools/deep_dive_thumbs.json"
ADOPTED = "p3b"


def main(argv):
    if argv and argv[0] == "--batch":
        conf = json.loads(Path(argv[1]).read_text())
        plans = [argv[argv.index("--plan") + 1]] if "--plan" in argv else PLANS
        for plan in plans:
            out = Path(conf["out"]) / plan
            out.mkdir(parents=True, exist_ok=True)
            for n, it in enumerate(conf["items"]):
                (out / f"LOG{it['log']}.html").write_text(render(it, plan, it.get("layout", n)))
            print(plan, len(conf["items"]))
        return
    conf = json.loads(DATA.read_text())
    items = conf["items"]
    if argv and argv[0] == "--add":
        log, question, key = int(argv[1]), argv[2], argv[3]
        items = [it for it in items if it["log"] != log]
        newest = max(items, key=lambda it: it["log"])
        # 一覧は LOG の大きい順に並ぶ。いちばん新しい記事の組みの1つ前を使うと、並んだときに4通りが順に回る
        items.append({"log": log, "question": question, "key": key, "layout": (newest["layout"] - 1) % 4})
        items.sort(key=lambda it: -it["log"])
        conf["items"] = items
        render(items[0] if items[0]["log"] == log else next(it for it in items if it["log"] == log), ADOPTED, 0)  # 1行の字数などを先に確かめる
        DATA.write_text(json.dumps(conf, ensure_ascii=False, indent=2) + "\n")
    out = SITE / "public/thumbs"
    out.mkdir(parents=True, exist_ok=True)
    for it in items:
        (out / f"LOG{it['log']}.html").write_text(render(it, ADOPTED, it["layout"]))
    print(f"書き出し {len(items)} 件 → public/thumbs/")


if __name__ == "__main__":
    main(sys.argv[1:])
