"""
design.md と同等の内容を design.xlsx として出力するビルドスクリプト。
各章を 1 シートにまとめ、表形式の項目はそのまま Excel テーブル化する。
Mermaid 図はテキストで記述する (詳細は design.md の Mermaid を参照)。
"""
from __future__ import annotations

from pathlib import Path
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter

OUT = Path(__file__).resolve().parent / "design.xlsx"

# ---------- スタイル定数 ----------
FONT_BASE = "Arial"
FONT_JP = "Yu Gothic"

C_HEADER_BG = "1F4E78"  # 紺
C_HEADER_FG = "FFFFFF"
C_SUBHEAD_BG = "D9E1F2"
C_TITLE_BG = "2F5496"
C_NOTE_BG = "FFF2CC"
C_OK = "70AD47"
C_NG = "C00000"
C_PENDING = "BF8F00"

thin = Side(style="thin", color="9C9C9C")
border_all = Border(left=thin, right=thin, top=thin, bottom=thin)


def set_font(cell, *, bold=False, color="000000", size=11, jp=True):
    cell.font = Font(name=FONT_JP if jp else FONT_BASE, bold=bold, color=color, size=size)


def header_cell(cell, text):
    cell.value = text
    cell.fill = PatternFill("solid", start_color=C_HEADER_BG)
    set_font(cell, bold=True, color=C_HEADER_FG)
    cell.alignment = Alignment(vertical="center", horizontal="center", wrap_text=True)
    cell.border = border_all


def data_cell(cell, text, *, wrap=True, bold=False, bg=None):
    cell.value = text
    if bg:
        cell.fill = PatternFill("solid", start_color=bg)
    set_font(cell, bold=bold)
    cell.alignment = Alignment(vertical="top", wrap_text=wrap)
    cell.border = border_all


def section_title(ws, row, text):
    ws.cell(row=row, column=1, value=text)
    cell = ws.cell(row=row, column=1)
    cell.fill = PatternFill("solid", start_color=C_TITLE_BG)
    set_font(cell, bold=True, color="FFFFFF", size=14)
    ws.row_dimensions[row].height = 24


def write_table(ws, start_row, headers, rows, col_widths=None):
    """ヘッダ+データ行で表を書く。次に書き込めるrowを返す。"""
    for i, h in enumerate(headers, start=1):
        header_cell(ws.cell(row=start_row, column=i), h)
    ws.row_dimensions[start_row].height = 30

    r = start_row + 1
    for row_data in rows:
        for i, v in enumerate(row_data, start=1):
            data_cell(ws.cell(row=r, column=i), v)
        r += 1

    if col_widths:
        for i, w in enumerate(col_widths, start=1):
            ws.column_dimensions[get_column_letter(i)].width = w
    return r + 1  # 次に使える行 (+1空け)


def write_paragraph(ws, row, text, *, bold=False, bg=None, span_cols=6):
    cell = ws.cell(row=row, column=1, value=text)
    ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=span_cols)
    if bg:
        cell.fill = PatternFill("solid", start_color=bg)
    set_font(cell, bold=bold)
    cell.alignment = Alignment(vertical="top", wrap_text=True)
    ws.row_dimensions[row].height = max(18, 18 * (len(text) // 60 + 1))
    return row + 1


# ============================================================
# ワークブック生成開始
# ============================================================
wb = Workbook()

# ------------------------------------------------------------
# 表紙
# ------------------------------------------------------------
ws = wb.active
ws.title = "表紙"
ws.column_dimensions["A"].width = 22
ws.column_dimensions["B"].width = 75

# タイトル
ws.merge_cells("A1:B1")
c = ws["A1"]
c.value = "JPKI Web 設計書"
c.fill = PatternFill("solid", start_color=C_TITLE_BG)
set_font(c, bold=True, color="FFFFFF", size=20)
c.alignment = Alignment(horizontal="center", vertical="center")
ws.row_dimensions[1].height = 50

meta = [
    ("プロジェクト名", "JPKI Web"),
    ("バージョン", "0.3.0"),
    ("文書バージョン", "1.0 (Initial)"),
    ("作成日", "2026-05-10"),
    ("著者", "highdefinitionaudiodriver"),
    ("ライセンス", "MIT"),
    ("リポジトリ", "https://github.com/highdefinitionaudiodriver/jpki-web"),
    ("関連文書", "docs/design.md (本書のソース版)"),
]
for i, (k, v) in enumerate(meta, start=3):
    header_cell(ws.cell(row=i, column=1), k)
    data_cell(ws.cell(row=i, column=2), v)
    ws.row_dimensions[i].height = 22

# 目次
toc_start = 12
ws.cell(row=toc_start, column=1, value="目次").fill = PatternFill("solid", start_color=C_SUBHEAD_BG)
set_font(ws.cell(row=toc_start, column=1), bold=True, size=12)
ws.merge_cells(start_row=toc_start, start_column=1, end_row=toc_start, end_column=2)
ws.row_dimensions[toc_start].height = 22

toc_items = [
    ("1", "はじめに"),
    ("2", "システム概要"),
    ("3", "機能要件"),
    ("4", "非機能要件"),
    ("5", "システム構成"),
    ("6", "画面設計"),
    ("7", "API 仕様"),
    ("8", "データ設計"),
    ("9", "処理フロー"),
    ("10", "モジュール設計"),
    ("11", "セキュリティ設計"),
    ("12", "エラーハンドリング設計"),
    ("13", "制限事項・既知の課題"),
    ("14", "ロードマップ"),
    ("15", "用語集"),
    ("付", "改版履歴"),
]
for i, (num, name) in enumerate(toc_items, start=toc_start + 1):
    data_cell(ws.cell(row=i, column=1), f"第{num}章")
    data_cell(ws.cell(row=i, column=2), name)

# 注記
note_row = toc_start + len(toc_items) + 2
write_paragraph(
    ws, note_row,
    "※ 本書は Mermaid 図を含む詳細版 docs/design.md の表形式版です。"
    "アーキテクチャ図 / シーケンス図 / 状態遷移図は docs/design.md にて閲覧してください。",
    bg=C_NOTE_BG, span_cols=2,
)

# ------------------------------------------------------------
# 1. はじめに
# ------------------------------------------------------------
ws = wb.create_sheet("1.はじめに")
ws.merge_cells("A1:D1")
ws.column_dimensions["A"].width = 22
ws.column_dimensions["B"].width = 30
ws.column_dimensions["C"].width = 25
ws.column_dimensions["D"].width = 60
section_title(ws, 1, "第1章 はじめに")

r = 3
r = write_paragraph(ws, r,
    "1.1 目的", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_paragraph(ws, r,
    "本書は、マイナンバーカード（JPKI）の署名用電子証明書を活用して、"
    "画像ファイルに電子署名を付与し、その真贋を検証する Web アプリケーション"
    "「JPKI Web」の設計を定義する。"
    "個人間（C2C）における合意・契約画像に対し、「誰が署名したか」および"
    "「画像が改ざんされていないか」を、国が発行する公的証明書を起点として担保することを目的とする。",
    span_cols=4)

r += 1
r = write_paragraph(ws, r, "1.2 スコープ", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["項目", "スコープ", "備考"],
    [
        ("画像 (PNG/JPEG) への JPKI 署名付与", "対象", ".jpkiimg コンテナとして出力"),
        (".jpkiimg の検証", "対象", "PKCS#7 + 署名者氏名抽出"),
        ("Web UI (検証/署名タブ)", "対象", "Vanilla JS"),
        ("同一PCローカル運用 (127.0.0.1)", "対象", "必須要件"),
        ("PDF (PAdES) 対応", "対象外", "今後の課題"),
        ("証明書チェーン/失効確認", "対象外", "今後の課題"),
        ("タイムスタンプトークン (RFC 3161)", "対象外", "今後の課題"),
        ("クラウド/SaaS 展開", "禁止", "PIN 漏洩リスクのため"),
    ],
    col_widths=[35, 12, 50])

r = write_paragraph(ws, r, "1.3 想定読者", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["想定読者", "目的"],
    [
        ("OSS コントリビュータ", "コード理解および PR 作成"),
        ("セキュリティレビュアー", "PIN 取り扱い・脅威モデルの確認"),
        ("利用者", "運用上の制約・前提条件の理解"),
    ],
    col_widths=[30, 60])

r = write_paragraph(ws, r, "1.4 参考文献", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["種別", "名称"],
    [
        ("RFC", "RFC 5652 (Cryptographic Message Syntax)"),
        ("RFC", "RFC 8017 (PKCS #1 v2.2)"),
        ("仕様", "公的個人認証サービス 署名用電子証明書 仕様 (J-LIS)"),
        ("規格", "ISO/IEC 7816-4 (APDU / Smart Card)"),
        ("規格", "PC/SC Workgroup Specifications"),
    ],
    col_widths=[15, 60])


# ------------------------------------------------------------
# 2. システム概要
# ------------------------------------------------------------
ws = wb.create_sheet("2.システム概要")
ws.column_dimensions["A"].width = 12
ws.column_dimensions["B"].width = 28
ws.column_dimensions["C"].width = 60
section_title(ws, 1, "第2章 システム概要")

r = 3
r = write_paragraph(ws, r, "2.1 コンテキスト (テキスト記述)", bold=True, bg=C_SUBHEAD_BG, span_cols=3)
r = write_paragraph(ws, r,
    "[利用者] → ブラウザ → [JPKI Web (127.0.0.1)] → [ICカードリーダー] → [マイナンバーカード]   "
    "[利用者] → .jpkiimg を送付 → [受領者] → ブラウザで検証   "
    "(視覚的な図は docs/design.md の Mermaid フローチャートを参照)",
    bg=C_NOTE_BG, span_cols=3)

r += 1
r = write_paragraph(ws, r, "2.2 アクター一覧", bold=True, bg=C_SUBHEAD_BG, span_cols=3)
r = write_table(ws, r,
    ["アクター", "役割"],
    [
        ("署名者", "画像と PIN を入力し、自分のマイナンバーカードで電子署名を行う"),
        ("受領者", "受け取った .jpkiimg を検証して、署名者と画像の真贋を確認する"),
        ("ローカル FastAPI", "ブラウザからの要求を受け、PC/SC 経由でカード操作・PKCS#7 構築を行う"),
        ("マイナンバーカード", "JPKI 署名用秘密鍵を保持し、PIN 認証後に RSA 署名を計算する"),
    ],
    col_widths=[25, 70])

r = write_paragraph(ws, r, "2.3 主要ユースケース", bold=True, bg=C_SUBHEAD_BG, span_cols=3)
r = write_table(ws, r,
    ["UC ID", "ユースケース", "アクター / 概要"],
    [
        ("UC-01", "画像に電子署名する", "署名者 / 画像と PIN を入力し、.jpkiimg を生成・ダウンロード"),
        ("UC-02", ".jpkiimg を検証する", "受領者 / アップロードして署名者氏名と改ざん有無を表示"),
        ("UC-03", "サーバー稼働状況を確認する", "利用者 / ヘルスチェック / ソフトロック状況を取得"),
    ],
    col_widths=[10, 28, 60])


# ------------------------------------------------------------
# 3. 機能要件
# ------------------------------------------------------------
ws = wb.create_sheet("3.機能要件")
ws.column_dimensions["A"].width = 10
ws.column_dimensions["B"].width = 20
ws.column_dimensions["C"].width = 55
ws.column_dimensions["D"].width = 25
section_title(ws, 1, "第3章 機能要件")

r = 3
r = write_paragraph(ws, r, "3.1 機能一覧", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["機能ID", "機能名", "概要", "エンドポイント"],
    [
        ("F-01", "画像署名", "PNG/JPEG にマイナンバーカードで電子署名を付与", "POST /api/sign"),
        ("F-02", "署名検証", ".jpkiimg の真贋判定および署名者情報抽出", "POST /api/verify"),
        ("F-03", "ヘルスチェック", "サーバー稼働確認 + 失敗カウンタ取得", "GET /api/health"),
        ("F-04", "静的ファイル配信", "Vanilla JS フロントエンド (index.html 等)", "GET /"),
        ("F-05", "タブUI", "検証 / 署名 のタブ切替", "フロントエンド"),
        ("F-06", "結果表示", "署名者氏名・有効期間・検証結果の表示", "フロントエンド"),
        ("F-07", "自動ダウンロード", "署名成功時に .jpkiimg を自動 DL", "フロントエンド"),
    ])

r = write_paragraph(ws, r, "3.2 機能詳細: F-01 画像署名", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["項目", "内容"],
    [
        ("入力", "画像（PNG/JPEG, ≤30MB） + PIN（6〜16桁 ASCII）"),
        ("処理",
            "(1) 入力検証 → (2) asyncio.Lock 取得 → (3) JPKI カード接続 → "
            "(4) PIN 残回数チェック → (5) VERIFY PIN → (6) SHA-256 + DigestInfo → "
            "(7) COMPUTE DIGITAL SIGNATURE → (8) 証明書読出 → (9) PKCS#7 構築 → "
            "(10) .jpkiimg 生成"),
        ("出力", "成功: .jpkiimg バイナリ (application/octet-stream) / 失敗: エラー JSON"),
        ("エラー条件", "PIN 不一致、PIN ロック、リーダー未検出、カード未挿入、サーバ側ソフトロック"),
    ],
    col_widths=[20, 90])

r = write_paragraph(ws, r, "3.3 機能詳細: F-02 署名検証", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["項目", "内容"],
    [
        ("入力", ".jpkiimg (≤50MB)"),
        ("処理", "(1) ZIP 展開 → (2) PKCS#7 パース → (3) 公開鍵で署名検証 → (4) 証明書から氏名抽出"),
        ("出力", "JSON（署名者氏名・有効期間・改ざん検知結果）"),
        ("エラー条件", "ZIP 形式不正、必須エントリ欠落、改ざん検知、PKCS#7 構造不正"),
    ],
    col_widths=[20, 90])


# ------------------------------------------------------------
# 4. 非機能要件
# ------------------------------------------------------------
ws = wb.create_sheet("4.非機能要件")
ws.column_dimensions["A"].width = 16
ws.column_dimensions["B"].width = 45
ws.column_dimensions["C"].width = 30
ws.column_dimensions["D"].width = 12
section_title(ws, 1, "第4章 非機能要件")

r = 3
r = write_paragraph(ws, r, "4.1 非機能要件マトリクス", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["カテゴリ", "要件", "目標値", "実装状況"],
    [
        ("性能", "検証応答時間", "≤ 1 秒（10MB以下）", "実装済"),
        ("性能", "署名応答時間", "≤ 5 秒（カード操作含む）", "実装済"),
        ("性能", "同時署名", "1 件（asyncio.Lock）", "実装済"),
        ("可用性", "稼働範囲", "ローカル実行中のみ", "実装済"),
        ("可用性", "ソフトロック", "3 回 PIN 失敗で停止", "実装済"),
        ("セキュリティ", "バインドアドレス", "127.0.0.1 のみ", "実装済"),
        ("セキュリティ", "CORS 許可", "localhost のみ", "実装済"),
        ("セキュリティ", "PIN ライフタイム", "受信→VERIFY→即破棄", "実装済"),
        ("セキュリティ", "PIN ロギング", "ログ・例外メッセージへの出力禁止", "実装済"),
        ("保守性", "ライセンス", "MIT", "実装済"),
        ("保守性", "コードカバレッジ", "将来目標 ≥ 70%", "未実装"),
        ("移植性", "OS 対応", "Windows / macOS / Linux", "実装済"),
        ("移植性", "Python", "3.12 以降", "実装済"),
        ("国際化", "言語", "日本語 UI", "実装済"),
    ])

r = write_paragraph(ws, r, "4.2 性能設計", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["項目", "設計"],
    [
        ("非同期処理", "重い同期処理（pyscard, cryptography）は loop.run_in_executor で別スレッド実行"),
        ("一時ファイル", "tempfile で隔離し、finally で確実に削除"),
        ("メモリ", "OS の一時ディレクトリに作成（メモリ温存）"),
    ],
    col_widths=[20, 90])

r = write_paragraph(ws, r, "4.3 運用要件", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["項目", "内容"],
    [
        ("起動方法", "python -m uvicorn app.main:app --host 127.0.0.1 --port 8000"),
        ("ログ出力", "標準出力（INFO レベル）"),
        ("監視", "/api/health で稼働状況確認可能"),
        ("バックアップ", "永続データなし（ステートレス）"),
    ],
    col_widths=[20, 90])


# ------------------------------------------------------------
# 5. システム構成
# ------------------------------------------------------------
ws = wb.create_sheet("5.システム構成")
ws.column_dimensions["A"].width = 18
ws.column_dimensions["B"].width = 25
ws.column_dimensions["C"].width = 60
section_title(ws, 1, "第5章 システム構成")

r = 3
r = write_paragraph(ws, r, "5.1 コンポーネント構成 (テキスト記述)", bold=True, bg=C_SUBHEAD_BG, span_cols=3)
r = write_paragraph(ws, r,
    "ブラウザ (Vanilla JS) ← HTTP → FastAPI (main.py)  "
    "FastAPI → service.py → {crypto/, container/, jpki/}  "
    "jpki/ → pyscard → OS の PC/SC → IC カードリーダー → マイナンバーカード  "
    "詳細は docs/design.md の Mermaid コンポーネント図を参照",
    bg=C_NOTE_BG, span_cols=3)

r += 1
r = write_paragraph(ws, r, "5.2 技術スタック", bold=True, bg=C_SUBHEAD_BG, span_cols=3)
r = write_table(ws, r,
    ["レイヤ", "技術", "バージョン"],
    [
        ("言語", "Python", "3.12+"),
        ("Web フレームワーク", "FastAPI", "0.136"),
        ("ASGI サーバ", "Uvicorn", "0.46 (with [standard])"),
        ("暗号", "cryptography", "≥ 42.0"),
        ("ASN.1", "asn1crypto", "≥ 1.5"),
        ("PC/SC", "pyscard", "≥ 2.0"),
        ("フロント", "Vanilla JS / HTML / CSS", "—"),
    ])

r = write_paragraph(ws, r, "5.3 デプロイ構成", bold=True, bg=C_SUBHEAD_BG, span_cols=3)
r = write_table(ws, r,
    ["構成要素", "場所/設定"],
    [
        ("ソースコード", "ユーザーPC: jpki-web/ (git clone)"),
        ("Python 仮想環境", ".venv/"),
        ("プロセス", "uvicorn (127.0.0.1:8000)"),
        ("IC カードリーダー", "物理 USB / NFC"),
        ("禁止構成", "クラウド／VPS／共有サーバ (PIN 漏洩リスクのため)"),
    ],
    col_widths=[25, 80])


# ------------------------------------------------------------
# 6. 画面設計
# ------------------------------------------------------------
ws = wb.create_sheet("6.画面設計")
ws.column_dimensions["A"].width = 10
ws.column_dimensions["B"].width = 25
ws.column_dimensions["C"].width = 20
ws.column_dimensions["D"].width = 10
ws.column_dimensions["E"].width = 45
section_title(ws, 1, "第6章 画面設計")

r = 3
r = write_paragraph(ws, r, "6.1 画面遷移", bold=True, bg=C_SUBHEAD_BG, span_cols=5)
r = write_paragraph(ws, r,
    "トップ画面(タブUI) → [🔍検証タブ] → 検証結果表示 → トップ画面  "
    "トップ画面(タブUI) → [✍️署名タブ] → 署名結果表示+自動DL → トップ画面",
    bg=C_NOTE_BG, span_cols=5)

r += 1
r = write_paragraph(ws, r, "6.2 画面項目: トップ画面 (タブUI 共通)", bold=True, bg=C_SUBHEAD_BG, span_cols=5)
r = write_table(ws, r,
    ["ID", "項目", "種別", "必須", "備考"],
    [
        ("H-01", "タイトル「JPKI Web」", "表示", "—", "—"),
        ("H-02", "サブタイトル", "表示", "—", "—"),
        ("H-03", "タブ「🔍 検証」", "ボタン", "—", "クリックで検証パネル表示"),
        ("H-04", "タブ「✍️ 署名」", "ボタン", "—", "クリックで署名パネル表示"),
    ])

r = write_paragraph(ws, r, "6.3 画面項目: 検証タブ", bold=True, bg=C_SUBHEAD_BG, span_cols=5)
r = write_table(ws, r,
    ["ID", "項目", "種別", "必須", "制約"],
    [
        ("V-01", "ファイル選択", "input[type=file]", "必須", ".jpkiimg / .zip"),
        ("V-02", "選択中ファイル情報", "表示", "—", "ファイル名 + サイズ"),
        ("V-03", "「署名を検証する」", "submit", "—", "押下で POST /api/verify"),
        ("V-04", "結果バナー", "表示", "—", "成功=緑 / 失敗=赤"),
        ("V-05", "詳細テーブル", "表示", "—", "氏名・有効期間 等 9 項目"),
        ("V-06", "生 JSON", "details", "—", "折りたたみ表示"),
    ])

r = write_paragraph(ws, r, "6.4 画面項目: 署名タブ", bold=True, bg=C_SUBHEAD_BG, span_cols=5)
r = write_table(ws, r,
    ["ID", "項目", "種別", "必須", "制約"],
    [
        ("S-00", "警告バナー（ローカル運用専用）", "表示", "—", "黄色背景"),
        ("S-01", "画像選択", "input[type=file]", "必須", "PNG / JPEG"),
        ("S-02", "選択中ファイル情報", "表示", "—", "—"),
        ("S-03", "PIN 入力", "input[type=password]", "必須", "6〜16 桁 ASCII"),
        ("S-04", "注意書き", "表示", "—", "「5 回ロック」「サーバ 3 回停止」"),
        ("S-05", "「署名して .jpkiimg を生成」", "submit", "—", "押下で POST /api/sign"),
        ("S-06", "結果バナー", "表示", "—", "—"),
        ("S-07", "詳細テーブル", "表示", "—", "生成ファイル名・サイズ・リーダー名"),
        ("S-08", "「⬇ .jpkiimg をダウンロード」", "a[download]", "—", "成功時のみ表示"),
        ("S-09", "エラー詳細", "details", "—", "エラー時に表示"),
    ])

r = write_paragraph(ws, r, "6.5 UI 規約", bold=True, bg=C_SUBHEAD_BG, span_cols=5)
r = write_table(ws, r,
    ["項目", "規約"],
    [
        ("配色", "ライトテーマ。成功=緑(#10b981)、失敗=赤(#ef4444)、警告=黄(#f59e0b)"),
        ("フォント", "システムフォントスタック + 日本語 (Hiragino Sans, Noto Sans JP)"),
        ("レスポンシブ", "コンテナ最大幅 760px、スマホは自動縮小"),
        ("アクセシビリティ", "ボタンのフォーカスリング、PIN欄は autocomplete='off'"),
    ],
    col_widths=[20, 80])


# ------------------------------------------------------------
# 7. API 仕様
# ------------------------------------------------------------
ws = wb.create_sheet("7.API仕様")
ws.column_dimensions["A"].width = 28
ws.column_dimensions["B"].width = 14
ws.column_dimensions["C"].width = 10
ws.column_dimensions["D"].width = 55
section_title(ws, 1, "第7章 API 仕様")

r = 3
r = write_paragraph(ws, r, "7.1 API 一覧", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["メソッド", "パス", "概要", "認証"],
    [
        ("GET", "/api/health", "稼働確認", "不要"),
        ("POST", "/api/verify", ".jpkiimg 検証", "不要"),
        ("POST", "/api/sign", "画像署名 → .jpkiimg 生成", "PIN 入力"),
        ("GET", "/ 配下", "静的ファイル", "不要"),
    ])

r = write_paragraph(ws, r, "7.2 共通仕様", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["項目", "内容"],
    [
        ("ベース URL", "http://127.0.0.1:8000"),
        ("Content-Type (req)", "multipart/form-data"),
        ("Content-Type (res)", "application/json または application/octet-stream"),
        ("文字コード", "UTF-8"),
    ],
    col_widths=[25, 85])

r = write_paragraph(ws, r, "7.3 POST /api/verify リクエスト", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["パラメータ", "型", "必須", "説明"],
    [
        ("file", "File", "必須", ".jpkiimg (≤50MB)"),
    ])

r = write_paragraph(ws, r, "7.4 POST /api/verify レスポンスフィールド (成功時)", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["フィールド", "型", "—", "説明"],
    [
        ("status", "string", "", "'success' / 'error'"),
        ("verified", "boolean", "", "署名が有効か"),
        ("signer.name", "string?", "", "JPKI 漢字氏名"),
        ("signer.name_source", "string", "", "san_jpki_other_name / san_directory_name / subject_cn / unknown"),
        ("signer.subject_cn", "string?", "", "Subject CN（JPKI では識別符号）"),
        ("signer.not_valid_before", "string?", "", "証明書有効期間 開始 (ISO 8601)"),
        ("signer.not_valid_after", "string?", "", "証明書有効期間 終了 (ISO 8601)"),
        ("image.name", "string", "", "コンテナ内画像ファイル名"),
        ("image.size", "number", "", "画像サイズ (bytes)"),
        ("file", "object", "", "アップロードファイルのメタ"),
        ("message", "string", "", "表示用メッセージ"),
        ("error_kind", "string?", "", "not_jpkiimg / missing_entry / tampered / structure / null"),
        ("checked_at", "string", "", "検証日時 ISO 8601 (+09:00)"),
    ])

r = write_paragraph(ws, r, "7.5 POST /api/sign リクエスト", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["パラメータ", "型", "必須", "説明"],
    [
        ("file", "File", "必須", "画像（PNG/JPEG, ≤30MB）"),
        ("pin", "string", "必須", "6〜16 桁 ASCII"),
    ])

r = write_paragraph(ws, r, "7.6 POST /api/sign レスポンス (成功)", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["項目", "値"],
    [
        ("Status", "200 OK"),
        ("Content-Type", "application/octet-stream"),
        ("Content-Disposition", "attachment; filename=\"<original>.jpkiimg\"; filename*=UTF-8''<encoded>"),
        ("X-JPKI-Container-Size", "バイト数"),
        ("X-JPKI-Reader", "リーダー名"),
        ("Body", ".jpkiimg バイナリ"),
    ],
    col_widths=[25, 85])

r = write_paragraph(ws, r, "7.7 POST /api/sign レスポンス (失敗 JSON)", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["フィールド", "型", "—", "説明"],
    [
        ("status", "string", "", "'error'"),
        ("error_kind", "string", "", "pin_failed / pin_locked / pin_risk / soft_locked / 等"),
        ("message", "string", "", "表示用メッセージ"),
        ("detail", "object?", "", "kind 固有の詳細 (例: remaining)"),
        ("checked_at", "string", "", "応答日時 ISO 8601"),
    ])


# ------------------------------------------------------------
# 8. データ設計
# ------------------------------------------------------------
ws = wb.create_sheet("8.データ設計")
ws.column_dimensions["A"].width = 28
ws.column_dimensions["B"].width = 12
ws.column_dimensions["C"].width = 18
ws.column_dimensions["D"].width = 55
section_title(ws, 1, "第8章 データ設計")

r = 3
r = write_paragraph(ws, r, "8.1 .jpkiimg コンテナ仕様 (ZIP_STORED)", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["エントリ", "必須", "圧縮", "内容"],
    [
        ("target_image.<ext>", "必須", "STORED", "元画像のバイト列（無加工 / ext=jpg|png|...）"),
        ("signature.p7s", "必須", "STORED", "PKCS#7 SignedData (detached) DER"),
        ("cert.der", "必須", "STORED", "X.509 証明書 DER（パディング除去済）"),
    ])

r = write_paragraph(ws, r, "8.2 PKCS#7 構造", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["階層", "値"],
    [
        ("ContentInfo.contentType", "signedData (1.2.840.113549.1.7.2)"),
        ("SignedData.version", "v1"),
        ("SignedData.digestAlgorithms", "{ SHA-256 }"),
        ("encapContentInfo.contentType", "data"),
        ("encapContentInfo.content", "ABSENT (detached)"),
        ("certificates", "[ X.509 cert ]"),
        ("SignerInfo.version", "v1 (IssuerAndSerialNumber 形式)"),
        ("SignerInfo.sid", "IssuerAndSerialNumber from cert"),
        ("SignerInfo.digestAlgorithm", "SHA-256"),
        ("SignerInfo.signedAttrs", "ABSENT (最小構成)"),
        ("SignerInfo.signatureAlgorithm", "rsaEncryption (1.2.840.113549.1.1.1)"),
        ("SignerInfo.signature", "256B RSA 署名値"),
    ],
    col_widths=[35, 90])

r = write_paragraph(ws, r,
    "特徴: signedAttrs を付加しない最小構成。RFC 5652 §5.4 に基づき eContent(=画像) の DigestInfo に直接対応する。"
    "これによりカードが返す raw RSA 署名値をそのまま格納可能。"
    "署名アルゴリズムは RSASSA-PKCS1-v1_5 + SHA-256 固定。",
    bg=C_NOTE_BG, span_cols=4)

r += 1
r = write_paragraph(ws, r, "8.3 DigestInfo (SHA-256) 51バイト構造", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["バイト列 (hex)", "意味"],
    [
        ("30 31", "SEQUENCE, length 49"),
        ("  30 0D", "  SEQUENCE, length 13"),
        ("    06 09 60 86 48 01 65 03 04 02 01", "    OID 2.16.840.1.101.3.4.2.1 (sha256)"),
        ("    05 00", "    NULL"),
        ("  04 20", "  OCTET STRING, length 32"),
        ("  <SHA-256 hash 32B>", "  画像のハッシュ"),
    ],
    col_widths=[45, 60])

r = write_paragraph(ws, r, "8.4 JPKI 署名用電子証明書 主要フィールド", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["フィールド", "OID", "—", "内容"],
    [
        ("Subject CN", "2.5.4.3", "", "識別符号（氏名ではなく、発行日時+乱数+連番で構成された一意 ID）"),
        ("SAN OtherName: 漢字氏名", "1.2.392.200149.8.5.5.1", "", "★ 本ツールが優先的に抽出"),
        ("SAN OtherName: 予備", "1.2.392.200149.8.5.5.2", "", "予備フィールド"),
        ("SAN OtherName: 性別", "1.2.392.200149.8.5.5.3", "", "1=男, 2=女"),
        ("SAN OtherName: 生年月日", "1.2.392.200149.8.5.5.4", "", "[元号区分][YYYYMMDD]"),
        ("SAN OtherName: 漢字住所", "1.2.392.200149.8.5.5.5", "", "住所（漢字）"),
        ("SAN OtherName: 補助番号", "1.2.392.200149.8.5.5.6", "", "17 桁"),
    ])

r = write_paragraph(ws, r, "8.5 氏名抽出優先順位", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["優先度", "取得元", "—", "備考"],
    [
        ("1", "SAN内 OtherName 氏名OID", "", "JPKI 仕様。本ツールの第一選択 (san_jpki_other_name)"),
        ("2", "SAN内 DirectoryName.CN", "", "他規格・将来規格への保険 (san_directory_name)"),
        ("3", "Subject CN", "", "JPKI では識別符号が入る (subject_cn) → 氏名取得失敗扱い"),
        ("4", "取得不能", "", "上記いずれも該当しない場合 (unknown)"),
    ])


# ------------------------------------------------------------
# 9. 処理フロー
# ------------------------------------------------------------
ws = wb.create_sheet("9.処理フロー")
ws.column_dimensions["A"].width = 8
ws.column_dimensions["B"].width = 20
ws.column_dimensions["C"].width = 25
ws.column_dimensions["D"].width = 60
section_title(ws, 1, "第9章 処理フロー")

r = 3
r = write_paragraph(ws, r,
    "本章は処理ステップを表形式で示す。シーケンス図の視覚版は docs/design.md の Mermaid を参照。",
    bg=C_NOTE_BG, span_cols=4)

r += 1
r = write_paragraph(ws, r, "9.1 署名フロー (POST /api/sign)", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["No.", "主体", "アクション", "備考"],
    [
        (1, "ブラウザ", "POST /api/sign multipart (file + pin)", ""),
        (2, "main.py", "入力検証 (拡張子/サイズ/PIN形式)", "Form(...) で取得"),
        (3, "main.py", "ソフトロック判定", "失敗カウント≥3 で 423"),
        (4, "main.py", "asyncio.Lock.acquire()", "競合時は 409 busy"),
        (5, "main.py", "executor で _sign_blocking 実行", "イベントループブロック回避"),
        (6, "service.py", "JpkiSession() で接続+AP SELECT", ""),
        (7, "JpkiSession", "assert_safe_to_attempt_pin()", "残回数<3 で例外 (pin_risk)"),
        (8, "JpkiSession", "VERIFY PIN", "成功時 sw=9000 / 失敗時 63CX"),
        (9, "JpkiSession", "失敗ハンドリング", "pin_failed / pin_locked"),
        (10, "service.py", "build_digest_info_sha256(image)", "51 バイトの DigestInfo"),
        (11, "JpkiSession", "COMPUTE DIGITAL SIGNATURE", "256B RSA 署名値取得"),
        (12, "JpkiSession", "署名用証明書 EF 読出", "READ BINARY ループ"),
        (13, "service.py", "trim_der(cert) で末尾パディング除去", ""),
        (14, "service.py", "build_p7s(sig, cert) → PKCS#7 構築", ""),
        (15, "service.py", "create_jpkiimg(...) → .jpkiimg 生成", "tempfile に作成"),
        (16, "main.py", "Lock.release() + 一時ファイル削除", "finally で確実に"),
        (17, "main.py", "Response (octet-stream) でブラウザに返却", "Content-Disposition 付き"),
        (18, "ブラウザ", "Blob 取得→自動ダウンロード", "URL.createObjectURL"),
        (19, "全層", "finally: PIN 変数を上書き+削除", "メモリ残存対策"),
    ])

r = write_paragraph(ws, r, "9.2 検証フロー (POST /api/verify)", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["No.", "主体", "アクション", "備考"],
    [
        (1, "ブラウザ", "POST /api/verify multipart (file)", ""),
        (2, "main.py", "入力検証 (拡張子/サイズ)", ""),
        (3, "service.py", "tempfile に保存", ""),
        (4, "container/reader", "read_jpkiimg() で ZIP 展開", "image / image_name / p7s / cert"),
        (5, "crypto/p7s", "verify_p7s_against_data(p7s, image)", "PKCS#7 検証"),
        (6, "crypto/p7s", "ContentInfo.load → SignedData", ""),
        (7, "crypto/p7s", "SignerInfo + 証明書照合", "issuer / serial 一致確認"),
        (8, "crypto/p7s", "公開鍵.verify(sig, image, PKCS1v15, SHA256)", "改ざん検知"),
        (9, "crypto/verify", "extract_signer_name(cert)", "SAN OtherName 優先抽出"),
        (10, "service.py", "VerifyServiceResult dict 組立", ""),
        (11, "main.py", "JSON でブラウザに返却", "200 OK"),
        (12, "service.py", "finally で一時ファイル削除", ""),
    ])

r = write_paragraph(ws, r, "9.3 PIN 失敗カウンタ 状態遷移", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["状態", "イベント", "遷移先", "備考"],
    [
        ("Normal (count=0)", "サーバ起動", "Normal", "初期状態"),
        ("Normal/Failure1/Failure2", "署名成功", "(同じ)", "リセット無し: 失敗履歴は累積"),
        ("Normal", "PIN 失敗", "Failure1 (count=1)", ""),
        ("Failure1", "PIN 失敗", "Failure2 (count=2)", ""),
        ("Failure2", "PIN 失敗", "SoftLocked (count=3)", "以降 /api/sign は 423"),
        ("SoftLocked", "/api/sign 呼出", "SoftLocked", "拒否継続"),
        ("SoftLocked", "サーバ再起動", "Normal", "プロセス再起動でリセット"),
    ])


# ------------------------------------------------------------
# 10. モジュール設計
# ------------------------------------------------------------
ws = wb.create_sheet("10.モジュール設計")
ws.column_dimensions["A"].width = 35
ws.column_dimensions["B"].width = 35
ws.column_dimensions["C"].width = 40
ws.column_dimensions["D"].width = 25
section_title(ws, 1, "第10章 モジュール設計")

r = 3
r = write_paragraph(ws, r, "10.1 ディレクトリ構造", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["パス", "種別", "—", "—"],
    [
        ("app/main.py", "API ルーティング層", "", ""),
        ("app/jpki_signer/__init__.py", "パッケージ初期化", "", ""),
        ("app/jpki_signer/service.py", "サービス層", "", ""),
        ("app/jpki_signer/crypto/der_utils.py", "DER ユーティリティ", "", ""),
        ("app/jpki_signer/crypto/p7s.py", "PKCS#7 構築・検証", "", ""),
        ("app/jpki_signer/crypto/verify.py", "高レベル検証 + 氏名抽出", "", ""),
        ("app/jpki_signer/container/reader.py", ".jpkiimg ZIP 読出", "", ""),
        ("app/jpki_signer/container/writer.py", ".jpkiimg ZIP 作成", "", ""),
        ("app/jpki_signer/jpki/apdu.py", "APDU 定数 / SW 解析", "", ""),
        ("app/jpki_signer/jpki/digest.py", "SHA-256 / DigestInfo", "", ""),
        ("app/jpki_signer/jpki/session.py", "JpkiSession クラス", "", ""),
        ("app/static/index.html", "フロントエンド画面", "", ""),
        ("app/static/app.js", "フロントエンド JS", "", ""),
        ("app/static/styles.css", "フロントエンド CSS", "", ""),
        ("app/uploads/", "一時ディレクトリ (gitignore)", "", ""),
        ("docs/design.md", "本書 (Markdown 版)", "", ""),
        ("docs/design.xlsx", "本書 (xlsx 版)", "", ""),
    ])

r = write_paragraph(ws, r, "10.2 レイヤ責務", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["レイヤ", "代表モジュール", "責務", "—"],
    [
        ("プレゼンテーション", "app/static/", "UI 表示・タブ切替・fetch 呼出・自動DL", ""),
        ("API", "main.py", "HTTP ルーティング / CORS / Lock / 失敗カウンタ / レスポンス整形", ""),
        ("サービス", "service.py", "ユースケース組立 / tempfile / エラー分類", ""),
        ("ドメイン", "container/, crypto/", "ZIP コンテナ R/W / PKCS#7 構築・検証", ""),
        ("デバイス", "jpki/", "APDU / カードセッション / PIN 管理", ""),
        ("外部", "pyscard, cryptography, asn1crypto", "PC/SC ライブラリ / 暗号ライブラリ", ""),
    ])

r = write_paragraph(ws, r, "10.3 モジュール責務一覧", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["モジュール", "公開関数", "責務", "依存"],
    [
        ("main.py", "FastAPI app", "HTTP ルーティング、CORS、Lock、失敗カウンタ、レスポンス整形",
            "service, FastAPI"),
        ("service.py", "verify_uploaded_jpkiimg, sign_image_to_jpkiimg",
            "検証・署名のユースケース全体を組み立てる。tempfile 管理、エラー分類",
            "container, crypto, jpki"),
        ("crypto/p7s.py", "build_p7s, verify_p7s_against_data, extract_signer_cert_der",
            "PKCS#7 SignedData の構築・検証、署名者証明書の抽出",
            "cryptography, asn1crypto"),
        ("crypto/verify.py", "verify_signed_image, extract_signer_name",
            "高レベル検証ラッパ + 署名者氏名抽出（JPKI SAN OtherName 対応）",
            "crypto.p7s, cryptography"),
        ("crypto/der_utils.py", "trim_der, actual_der_length",
            "DER 長さ算出・末尾パディング除去", "—"),
        ("container/writer.py", "create_jpkiimg",
            ".jpkiimg ZIP 作成", "zipfile"),
        ("container/reader.py", "read_jpkiimg",
            ".jpkiimg ZIP 読出 + 例外", "zipfile"),
        ("jpki/session.py", "JpkiSession, 例外群",
            "JPKI セッション（接続/AP SELECT/VERIFY PIN/署名/証明書読出）",
            "pyscard, jpki.apdu"),
        ("jpki/apdu.py", "SELECT_*, parse_sw",
            "APDU 定数 + SW 解析", "smartcard.util"),
        ("jpki/digest.py", "build_digest_info_sha256",
            "SHA-256 + DigestInfo 構築", "hashlib"),
    ])

r = write_paragraph(ws, r, "10.4 主要クラス JpkiSession", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["カテゴリ", "メンバ", "—", "説明"],
    [
        ("状態", "_conn", "", "PC/SC connection"),
        ("状態", "_reader_name", "", "リーダー名 (str)"),
        ("状態", "_atr", "", "ATR (bytes)"),
        ("状態", "_is_pin_verified", "", "PIN 認証フラグ (bool)"),
        ("ライフサイクル", "__init__(reader_index)", "", "接続 + SELECT JPKI AP"),
        ("ライフサイクル", "__enter__/__exit__", "", "with 文対応"),
        ("ライフサイクル", "disconnect()", "", "切断"),
        ("PIN", "get_pin_remaining()", "", "残回数取得 (試行消費なし)"),
        ("PIN", "assert_safe_to_attempt_pin(threshold=3)", "", "残回数<閾値で例外"),
        ("PIN", "verify_pin(pin)", "", "VERIFY PIN (bytearray ゼロクリア付き)"),
        ("署名", "sign_digest_info(di)", "", "COMPUTE DIGITAL SIGNATURE (PIN 必須)"),
        ("署名", "read_sign_certificate()", "", "署名用証明書 EF 読出 (PIN 必須)"),
        ("補助", "read_auth_certificate()", "", "利用者証明用証明書 (PIN 不要)"),
    ])


# ------------------------------------------------------------
# 11. セキュリティ設計
# ------------------------------------------------------------
ws = wb.create_sheet("11.セキュリティ設計")
ws.column_dimensions["A"].width = 10
ws.column_dimensions["B"].width = 40
ws.column_dimensions["C"].width = 18
ws.column_dimensions["D"].width = 12
ws.column_dimensions["E"].width = 45
section_title(ws, 1, "第11章 セキュリティ設計")

r = 3
r = write_paragraph(ws, r, "11.1 脅威モデル", bold=True, bg=C_SUBHEAD_BG, span_cols=5)
r = write_table(ws, r,
    ["脅威ID", "脅威", "想定攻撃者", "影響度", "対策"],
    [
        ("T-01", "PIN がネットワーク越しに盗聴される", "中間者", "致命的",
            "127.0.0.1 バインドのみ、README で必須化"),
        ("T-02", "PIN がログに記録される", "サーバ管理者", "高",
            "PIN を pin 以外の変数に格納しない、ログ出力時に除外"),
        ("T-03", "PIN がプロセスメモリに残存", "フォレンジック", "中",
            "bytearray ゼロクリア + del"),
        ("T-04", "カードがロックされる（DoS）", "不正利用者", "高",
            "残回数<3 で送信前中止、サーバ側ソフトロック"),
        ("T-05", "複数タブからの同時署名", "利用者", "中",
            "asyncio.Lock でシリアライズ"),
        ("T-06", "クロスサイトリクエスト", "悪意のあるサイト", "高",
            "CORS allow_origins を 127.0.0.1/localhost 限定、Cookie 不使用"),
        ("T-07", "個人情報の漏洩（証明書）", "不注意な利用者", "高",
            ".gitignore 強化、README でプライバシー警告"),
        ("T-08", "改ざんされた .jpkiimg の悪用", "偽造者", "中",
            "PKCS#7 検証で 1 ビットの改変も検出"),
    ])

r = write_paragraph(ws, r, "11.2 PIN ライフタイム (テキスト記述)", bold=True, bg=C_SUBHEAD_BG, span_cols=5)
r = write_table(ws, r,
    ["No.", "主体", "—", "—", "アクション"],
    [
        (1, "ブラウザ", "", "", "ユーザー入力 → POST body (multipart) の pin フィールドで送信"),
        (2, "main.py", "", "", "pin 変数で保持 (ログ・例外には絶対に含めない)"),
        (3, "main.py", "", "", "run_in_executor(_sign_blocking, image, filename, pin)"),
        (4, "JpkiSession", "", "", "bytearray にコピー → APDU 送信 → finally でゼロクリア + del"),
        (5, "service.py", "", "", "finally: pin = '0'*len(pin); del pin"),
        (6, "main.py", "", "", "finally: pin = '0'*len(pin); del pin"),
    ])

r = write_paragraph(ws, r, "11.3 多層防衛", bold=True, bg=C_SUBHEAD_BG, span_cols=5)
r = write_table(ws, r,
    ["層", "対策", "—", "—", "実装箇所"],
    [
        ("ネットワーク", "バインドアドレス制限", "", "", "README + 利用者責任"),
        ("HTTP", "CORS allow_origins 限定", "", "", "main.py CORSMiddleware"),
        ("HTTP", "クエリ文字列ではなく body 経由", "", "", "Form(...) のみ受付"),
        ("アプリ", "PIN 形式バリデーション", "", "", "service.py"),
        ("アプリ", "サーバ側ソフトロック", "", "", "main.py _sign_failure_count"),
        ("アプリ", "同時実行制御", "", "", "main.py SIGN_LOCK"),
        ("アプリ", "カード側残回数チェック", "", "", "JpkiSession.assert_safe_to_attempt_pin()"),
        ("カード", "OS による物理ロック", "", "", "カード側 5 回失敗で永久ロック"),
        ("メモリ", "bytearray ゼロクリア", "", "", "JpkiSession.verify_pin()"),
        ("メモリ", "str 変数の上書き＋削除", "", "", "main.py, service.py"),
    ])

r = write_paragraph(ws, r, "11.4 機密データ取り扱いルール", bold=True, bg=C_SUBHEAD_BG, span_cols=5)
r = write_table(ws, r,
    ["データ", "取り扱い", "—", "—", "—"],
    [
        ("PIN", "ログ出力禁止、例外メッセージに含めない、即破棄", "", "", ""),
        ("証明書（個人情報含む）", "レスポンスに含めるのは必要最小限（氏名・有効期間）", "", "", ""),
        (".jpkiimg", "サーバに永続化しない（tempfile で生成→送信→削除）", "", "", ""),
        ("元画像", "サーバに永続化しない（同上）", "", "", ""),
    ])


# ------------------------------------------------------------
# 12. エラーハンドリング設計
# ------------------------------------------------------------
ws = wb.create_sheet("12.エラーハンドリング")
ws.column_dimensions["A"].width = 22
ws.column_dimensions["B"].width = 8
ws.column_dimensions["C"].width = 10
ws.column_dimensions["D"].width = 35
ws.column_dimensions["E"].width = 40
section_title(ws, 1, "第12章 エラーハンドリング設計")

r = 3
r = write_paragraph(ws, r, "12.1 エラー種別 (error_kind) 一覧", bold=True, bg=C_SUBHEAD_BG, span_cols=5)
r = write_table(ws, r,
    ["kind", "HTTP", "API", "説明", "主な発生条件"],
    [
        ("not_jpkiimg", "200", "verify", "ZIP として開けない", "拡張子は合っているがバイナリ不正"),
        ("missing_entry", "200", "verify", "必須エントリ欠落", "signature.p7s や cert.der が無い"),
        ("tampered", "200", "verify", "改ざん検知", "PKCS#7 検証で署名値とハッシュが不一致"),
        ("structure", "200", "verify", "証明書/構造の解析失敗", "PKCS#7 や証明書の DER パース失敗"),
        ("empty_image", "400", "sign", "空ファイル", "アップロード本体が空"),
        ("missing_filename", "400", "sign", "ファイル名取得不能", "クライアント不具合"),
        ("invalid_pin_format", "400", "sign", "PIN 形式不正", "桁数 / ASCII 違反"),
        ("unsupported_extension", "415", "sign", "拡張子未対応", "PNG/JPEG 以外"),
        ("too_large", "413", "sign", "サイズ超過", "30MB 超"),
        ("pin_failed", "401", "sign", "PIN 不一致", "カードが SW=63CX を返した"),
        ("pin_locked", "423", "sign", "カード側ロック", "カードが SW=6983 を返した"),
        ("pin_risk", "409", "sign", "残回数<3 で送信前中止", "安全装置作動"),
        ("soft_locked", "423", "sign", "サーバ側ソフトロック", "プロセス内 3 回失敗到達"),
        ("no_reader", "503", "sign", "リーダー未検出", "pyscard でリーダー無し"),
        ("card_error", "502", "sign", "カード通信失敗", "想定外 SW / 切断"),
        ("pyscard_unavailable", "500", "sign", "pyscard 未インストール", "import エラー"),
        ("busy", "409", "sign", "他処理中", "SIGN_LOCK 競合"),
        ("internal_error", "500", "sign", "予期しないエラー", "上記以外"),
    ])

r = write_paragraph(ws, r, "12.2 例外クラス階層", bold=True, bg=C_SUBHEAD_BG, span_cols=5)
r = write_table(ws, r,
    ["階層", "例外クラス", "対応 kind", "—", "備考"],
    [
        ("1", "SignError", "(全 sign 系)", "", "本プロジェクト独自 (kind, http_status, extra 保持)"),
        ("1", "JpkiError", "(基底)", "", "jpki-image-signer 由来"),
        ("2", "JpkiNoReaderError", "no_reader (503)", "", ""),
        ("2", "JpkiCardError", "card_error (502)", "", ""),
        ("2", "JpkiPinError", "(基底)", "", ""),
        ("3", "JpkiPinLockedError", "pin_locked (423)", "", ""),
        ("3", "JpkiPinFailedError", "pin_failed (401)", "", "remaining 属性付き"),
        ("3", "JpkiPinRiskError", "pin_risk (409)", "", "remaining/threshold 属性付き"),
        ("2", "JpkiPinNotVerifiedError", "internal_error (500)", "", "内部不整合"),
        ("1", "P7sError / P7sVerificationError", "structure", "", "jpki-image-signer 由来"),
    ])

r = write_paragraph(ws, r, "12.3 ログ出力ルール", bold=True, bg=C_SUBHEAD_BG, span_cols=5)
r = write_table(ws, r,
    ["レベル", "出力対象", "PIN を含めて良いか", "—", "—"],
    [
        ("INFO", "リクエスト受信、署名成功サマリ", "❌ 禁止", "", ""),
        ("WARN", "エラー応答（kind と message のみ）", "❌ 禁止", "", ""),
        ("EXCEPTION", "予期しない例外（stack trace）", "❌ 禁止", "", ""),
        ("DEBUG", "開発時の APDU 詳細", "❌ 禁止", "", ""),
    ])


# ------------------------------------------------------------
# 13. 制限事項
# ------------------------------------------------------------
ws = wb.create_sheet("13.制限事項")
ws.column_dimensions["A"].width = 8
ws.column_dimensions["B"].width = 35
ws.column_dimensions["C"].width = 40
ws.column_dimensions["D"].width = 35
section_title(ws, 1, "第13章 制限事項・既知の課題")

r = 3
r = write_paragraph(ws, r, "13.1 現バージョンの制限", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["ID", "制限", "影響", "回避策"],
    [
        ("L-01", "対応形式は .jpkiimg のみ", "PDF やオフィス文書には未対応", "画像化してから署名"),
        ("L-02", "証明書チェーン未検証", "偽造証明書を含む .jpkiimg でも形式上 OK と判定し得る",
            "別途 JPKI ルート CA で検証"),
        ("L-03", "CRL/OCSP 未確認", "失効済み証明書を使った署名でも有効と判定",
            "受領後に J-LIS の有効性確認サービスを利用"),
        ("L-04", "RFC 3161 TST 未対応", "「いつ署名したか」の第三者証明なし",
            "別途タイムスタンプ機関を利用"),
        ("L-05", "利用者証明用証明書 (auth) 未活用", "認証用途には未対応", "—"),
        ("L-06", "複数リーダー時の選択 UI 無し", "複数台環境で署名不可", "1 台のみ接続にする"),
        ("L-07", "リーダー/カード状態の UI 通知無し", "接続前に PIN 入力可能", "—"),
    ])

r = write_paragraph(ws, r, "13.2 Python 環境注意", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["項目", "内容"],
    [
        ("推奨バージョン", "Python 3.12"),
        ("Python 3.14", "pyscard wheel 未提供 (要 SWIG + MSVC Build Tools)"),
    ],
    col_widths=[25, 80])

r = write_paragraph(ws, r, "13.3 NFC リーダー固有の挙動", bold=True, bg=C_SUBHEAD_BG, span_cols=4)
r = write_table(ws, r,
    ["項目", "内容"],
    [
        ("NFC ポートソフトウェア", "一部 NFC リーダーは J-LIS の NFC ポートソフトウェアのインストールが必要"),
        ("運用注意", "カードを「かざし続ける」運用が必要"),
    ],
    col_widths=[25, 80])


# ------------------------------------------------------------
# 14. ロードマップ
# ------------------------------------------------------------
ws = wb.create_sheet("14.ロードマップ")
ws.column_dimensions["A"].width = 12
ws.column_dimensions["B"].width = 50
ws.column_dimensions["C"].width = 12
ws.column_dimensions["D"].width = 12
section_title(ws, 1, "第14章 ロードマップ")

r = 3
r = write_table(ws, r,
    ["Phase", "内容", "優先度", "状態"],
    [
        ("完了", "MVP（検証/署名/タブUI）", "—", "✅"),
        ("完了", "OSS 公開（MIT License）", "—", "✅"),
        ("完了", "本設計書作成", "—", "✅"),
        ("次期", "証明書チェーン検証 + CRL/OCSP", "高", "⏳"),
        ("次期", "pytest 自動テスト + GitHub Actions CI", "高", "⏳"),
        ("中期", "PDF (PAdES) 形式対応（pyhanko 検討）", "中", "⏳"),
        ("中期", "タイムスタンプトークン (RFC 3161)", "中", "⏳"),
        ("中期", "複数リーダー対応 UI", "中", "⏳"),
        ("長期", "Electron スタンドアロン化", "低", "⏳"),
        ("長期", "利用者証明用証明書 (auth) を使った認証フロー", "低", "⏳"),
    ])


# ------------------------------------------------------------
# 15. 用語集
# ------------------------------------------------------------
ws = wb.create_sheet("15.用語集")
ws.column_dimensions["A"].width = 28
ws.column_dimensions["B"].width = 80
section_title(ws, 1, "第15章 用語集")

r = 3
r = write_table(ws, r,
    ["用語", "説明"],
    [
        ("JPKI", "Japanese Public Key Infrastructure。公的個人認証サービス"),
        ("J-LIS", "地方公共団体情報システム機構（JPKI 運営主体）"),
        ("マイナンバーカード", "JPKI 署名用・利用者証明用の電子証明書を格納する IC カード"),
        ("署名用電子証明書", "行政手続きや電子契約に使う署名鍵に紐づく証明書（5 年有効）"),
        ("PKCS#7 / CMS",
            "RFC 5652 の Cryptographic Message Syntax。本ツールは SignedData (detached) を使用"),
        ("PAdES", "PDF Advanced Electronic Signatures。PDF に電子署名を埋め込む仕様"),
        ("PC/SC", "PC/Smart Card。OS とカードリーダーの標準インタフェース"),
        ("APDU", "Application Protocol Data Unit。カードとの通信単位（ISO 7816-4）"),
        ("DigestInfo", "RFC 8017 で定義されるハッシュ値の ASN.1 ラッパ"),
        (".jpkiimg", "本プロジェクト独自の ZIP コンテナ拡張子"),
        ("識別符号", "JPKI 仕様で Subject CN に格納される一意 ID（氏名ではない）"),
        ("SAN", "Subject Alternative Name。証明書拡張領域"),
        ("OtherName", "SAN の選択肢の一つで、任意 OID で値を格納できる形式"),
        ("pyscard", "Python の PC/SC バインディング"),
        ("cryptography", "Python の暗号ライブラリ（PyCA）"),
        ("asn1crypto", "Python の純粋実装 ASN.1 ライブラリ"),
        ("ATR", "Answer To Reset。カード接続時にカードが返す識別バイト列"),
        ("CRL", "Certificate Revocation List。証明書失効リスト"),
        ("OCSP", "Online Certificate Status Protocol。証明書有効性のオンライン照会"),
        ("TST", "TimeStamp Token。RFC 3161 のタイムスタンプ"),
    ])


# ------------------------------------------------------------
# 改版履歴
# ------------------------------------------------------------
ws = wb.create_sheet("改版履歴")
ws.column_dimensions["A"].width = 10
ws.column_dimensions["B"].width = 15
ws.column_dimensions["C"].width = 30
ws.column_dimensions["D"].width = 60
section_title(ws, 1, "改版履歴")

r = 3
r = write_table(ws, r,
    ["版", "日付", "著者", "内容"],
    [
        ("1.0", "2026-05-10", "highdefinitionaudiodriver", "初版作成（jpki-web v0.3.0 に対応）"),
    ])


# ------------------------------------------------------------
# 保存
# ------------------------------------------------------------
wb.save(OUT)
print(f"OK: {OUT} ({OUT.stat().st_size:,} bytes)")
