# -*- coding: utf-8 -*-
"""
TeamWorks 宛 確認事項（第7回）を PDF にする。

第1回〜第6回（make_ddl_review_pdf.py / make_teamworks_qa2_pdf.py 〜
make_teamworks_qa6_pdf.py）の体裁をそのまま踏襲した続編。
A4 縦 / reportlab / 日本語フォント埋め込み / ヘッダー罫線とページ番号。

フォントは BIZ UDGothic（日本語フォント）を埋め込む。
中国語向けフォントは漢字の字形が異なるため使わない。
フォールバックにも混ぜないこと。

BIZ UDGothic は等幅（半角は 0.5em 固定）なので、SQL の枠にもこのフォントを使う。
コードブロック内の空白は &nbsp; に置換して桁を保持する。

原稿は docs/handoff/TeamWorks確認事項_第7回_20260910.md。
文言・件数・表の内容は原稿から変更していない。

★2026-09-10 の改訂（送付直前）★
  1. 旧第4部（ダンプ元サーバが MariaDB に変わっている件）を削除した。
     送付前に「本番サーバは MariaDB です」とご回答をいただけたため。
     謝意と当方の対応状況は第1部 1-4 に移した。旧5→4、旧6→5 に繰り上げ。
  2. 第3部の依頼から B-1（clean_timetable_id の外部キー／索引）と
     A-1（主キー）を落とし、A-2 / A-3b / B-2 / B-3b の4件だけにした。
     取り下げの経緯は第1部 1-5（共有）に書いた。
     理由: prismaSource.cleanLogs()（lib/db/index.ts:186-237）が
     cleanTimetable / cleanStatusHistory / robotErrorReport を
     WHERE 句なしの全件取得で読み、取得後に Map でメモリ結合しているため、
     clean_timetable_id で「検索」しておらず索引が効く場所が無い。
  3. 質問は3件（接続先 / DDL の未反映4件 / precheck の値域）。
     第5部（面積）は質問を取り下げて共有のみにした。

★列幅の決め方★
wordWrap="CJK" は ASCII の語中でも改行する。そのため
`robot_task_versions.clean_schedule_id` /
`security_status_histories.reported_at` /
`clean_watch_20260909.sql` / `clean_status_histories.id` /
`facility_contacts.email` / `@prisma/adapter-mariadb` /
`5.5.5-10.11.13-MariaDB-log` / `clean_timetables.precheck`
のような長い識別子は
  (1) 行頭に来るよう <br/> で明示的に改行してから置く
  (2) 列の内寸（列幅 - 左右パディング 12pt）が識別子の幅
      （BIZ UDGothic は半角 0.5em なので フォントサイズ × 文字数 ÷ 2）
      を上回るように列幅を決める
の2点で語中改行を避けている。check_teamworks_qa7_pdf.py の
「4. 識別子の語中改行」がこれを機械的に検査する。
"""
import os

from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.fonts import addMapping
from reportlab.platypus import (
    BaseDocTemplate, CondPageBreak, Frame, PageTemplate, Paragraph, Spacer,
    Table, TableStyle,
)

FONT_DIR = "C:/Windows/Fonts/"
pdfmetrics.registerFont(TTFont("JP", FONT_DIR + "BIZ-UDGothicR.ttc", subfontIndex=0))
pdfmetrics.registerFont(TTFont("JP-B", FONT_DIR + "BIZ-UDGothicB.ttc", subfontIndex=0))
# <b> タグを太字フォントへ対応づける
addMapping("JP", 0, 0, "JP")
addMapping("JP", 1, 0, "JP-B")

# 出力先。既定はこのスクリプトと同じディレクトリ。
# 環境変数 DWATCH_PDF_OUT で変えられる（例: 送付前に Downloads へ出す）。
OUT_DIR = os.environ.get("DWATCH_PDF_OUT",
                         os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(OUT_DIR, "DWatch_確認事項_第7回_20260910.pdf")

# 第1回 PDF と同一のパレット
INK = colors.HexColor("#1e293b")
SUB = colors.HexColor("#475569")
MUT = colors.HexColor("#94a3b8")
LINE = colors.HexColor("#cbd5e1")
HEAD_BG = colors.HexColor("#f1f5f9")
CODE_BG = colors.HexColor("#f8fafc")
NG = colors.HexColor("#b91c1c")   # 実装が止まる区分の強調色
WN = colors.HexColor("#b45309")   # 定義の確認区分の強調色

ss = getSampleStyleSheet()


def st(name, size, leading, font="JP", color=INK, space_after=0, space_before=0,
       left=0, first=0):
    """段落スタイルを作る。left/first でぶら下げインデントを表現する。"""
    s = ParagraphStyle(
        name, parent=ss["Normal"], fontName=font, fontSize=size, leading=leading,
        textColor=color, alignment=TA_LEFT, spaceAfter=space_after,
        spaceBefore=space_before, wordWrap="CJK",
        leftIndent=left, firstLineIndent=first,
    )
    s.bulletFontName = font
    return s


S_TITLE = st("t", 17, 24, "JP-B", INK, 2)
S_SUB = st("s", 9.5, 15, "JP", SUB, 0)
S_H2 = st("h2", 12.5, 18, "JP-B", INK, 4, 10)
S_H3 = st("h3", 10.5, 16, "JP-B", INK, 3, 8)
S_H4 = st("h4", 9.8, 15, "JP-B", SUB, 2, 6)
S_BODY = st("b", 9.5, 15.5, "JP", INK, 4)
S_LEAD = st("ld", 9.5, 15.5, "JP", INK, 2)          # 直後にコード枠や表が続く導入文
S_NOTE = st("n", 9, 14.5, "JP", SUB, 4)
S_LIST = st("li", 9.5, 15.5, "JP", INK, 2, 0, 15, -15)   # 「1. 」のぶら下げ
S_BULL = st("bu", 9.5, 15.5, "JP", INK, 2, 0, 14, -14)   # 「・」のぶら下げ
S_CELL = st("c", 8.8, 13.8, "JP", INK)
S_CELL_S = st("cs", 8.8, 13.8, "JP", SUB)
S_CODE = st("cd", 8.2, 12.6, "JP", INK)
S_TH = st("th", 8.4, 12, "JP-B", SUB)
S_QUOTE = st("q", 9.2, 14.8, "JP", SUB)
S_CALL = st("ca", 10.5, 17, "JP-B", INK)   # 目立たせたい一文を囲む枠の中身


def P(t, s=S_CELL):
    return Paragraph(t, s)


def on_page(canvas, doc):
    """全ページ共通のヘッダー罫線・フッター・ページ番号。第1回の PDF と同一。"""
    canvas.saveState()
    canvas.setFont("JP", 8)
    canvas.setFillColor(MUT)
    canvas.drawString(18 * mm, 12 * mm, "D-Watch / DFA Robotics")
    canvas.drawRightString(A4[0] - 18 * mm, 12 * mm, "%d" % doc.page)
    canvas.setStrokeColor(LINE)
    canvas.setLineWidth(0.4)
    canvas.line(18 * mm, 15.5 * mm, A4[0] - 18 * mm, 15.5 * mm)
    canvas.restoreState()


TBL_STYLE = [
    ("FONTNAME", (0, 0), (-1, -1), "JP"),
    ("BACKGROUND", (0, 0), (-1, 0), HEAD_BG),
    ("LINEBELOW", (0, 0), (-1, 0), 0.6, LINE),
    ("LINEBELOW", (0, 1), (-1, -2), 0.3, LINE),
    ("VALIGN", (0, 0), (-1, -1), "TOP"),
    ("TOPPADDING", (0, 0), (-1, -1), 5),
    ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
    ("LEFTPADDING", (0, 0), (-1, -1), 6),
    ("RIGHTPADDING", (0, 0), (-1, -1), 6),
]


def table(rows, widths, cell_styles=None, head_color=None):
    """rows[0] を見出し行とする表。cell_styles で列ごとの段落スタイルを指定する。"""
    if cell_styles is None:
        cell_styles = [S_CELL] * len(rows[0])
    data = [[P(c, S_TH) for c in rows[0]]]
    for r in rows[1:]:
        data.append([P(c, cell_styles[i]) for i, c in enumerate(r)])
    t = Table(data, colWidths=widths, repeatRows=1)
    style = list(TBL_STYLE)
    if head_color is not None:
        style.append(("TEXTCOLOR", (0, 1), (0, -1), head_color))
    t.setStyle(TableStyle(style))
    return t


def code(lines, width):
    """SQL などを等幅フォントの枠に入れる。空白は &nbsp; にして桁を保つ。"""
    esc = []
    for ln in lines:
        ln = ln.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
        esc.append(ln.replace(" ", "&nbsp;"))
    t = Table([[P("<br/>".join(esc), S_CODE)]], colWidths=[width])
    t.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "JP"),
        ("BACKGROUND", (0, 0), (-1, -1), CODE_BG),
        ("LINEBEFORE", (0, 0), (0, -1), 1.8, LINE),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("TOPPADDING", (0, 0), (-1, -1), 6),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 6),
        ("LEFTPADDING", (0, 0), (-1, -1), 8),
        ("RIGHTPADDING", (0, 0), (-1, -1), 6),
    ]))
    return t


def quote(text, width):
    """いただいた回答の引用。左に縦罫線を引く。"""
    t = Table([[P(text, S_QUOTE)]], colWidths=[width])
    t.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "JP"),
        ("LINEBEFORE", (0, 0), (0, -1), 1.8, MUT),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("TOPPADDING", (0, 0), (-1, -1), 3),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 3),
        ("LEFTPADDING", (0, 0), (-1, -1), 8),
        ("RIGHTPADDING", (0, 0), (-1, -1), 0),
    ]))
    return t


def callout(text, width):
    """一番伝えたい一文を薄い背景の枠に入れる（引用ではないので本文色）。"""
    t = Table([[P(text, S_CALL)]], colWidths=[width])
    t.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "JP"),
        ("BACKGROUND", (0, 0), (-1, -1), CODE_BG),
        ("LINEBEFORE", (0, 0), (0, -1), 1.8, MUT),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("TOPPADDING", (0, 0), (-1, -1), 7),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 7),
        ("LEFTPADDING", (0, 0), (-1, -1), 10),
        ("RIGHTPADDING", (0, 0), (-1, -1), 8),
    ]))
    return t


def num(items, start=1):
    """「1. 」形式の番号付きリスト。"""
    return [Paragraph("%d. %s" % (i, t), S_LIST)
            for i, t in enumerate(items, start)]


def bull(items):
    """「・」形式の箇条書き。"""
    return [Paragraph("・%s" % t, S_BULL) for t in items]


# ---------------------------------------------------------------- 表データ
# 前文: 5部構成（旧第4部の MariaDB は削除済み。改訂1）
SUMMARY_ROWS = [
    ["#", "内容", "優先度"],
    ["1. 照合結果のご共有",
     "いただいたDDLを当日中に照合しました。<br/>"
     "MariaDB の件と、<b>依頼2件の取り下げ</b>もこちらです",
     "<b>回答不要</b>"],
    ["2. 接続先<br/>（ホスト名・ポート）",
     "<b>第6回からの継続。あとはこれだけです</b>", "<b>最優先</b>"],
    ["3. DDL の未反映項目<br/>（4件）",
     "採番・桁数・DEFAULT・索引の食い違いです", "<b>お手すきの折に</b>"],
    ["4. precheck に入る<br/>値の範囲", "反映時期と既存データの扱い", "中"],
    ["5. 面積の型変更について", "<b>共有のみ。質問はありません</b>",
     "<b>回答不要</b>"],
]

# 1-4. 本番が MariaDB であることのご教示への謝意と、当方の対応状況
MARIA_ROWS = [
    ["#", "内容"],
    ["1", "<b>アプリの実装に生の SQL は1件もありません。</b>"
          "すべて Prisma のクエリビルダ経由ですので、"
          "<b>現時点で対応が必要な箇所はありません</b>"],
    ["2", "<b>DBクライアントのドライバも、すでに MariaDB 用のもの"
          "（@prisma/adapter-mariadb）を使っております</b>"],
    ["3", "<b>「どうしても MariaDB 特有の DML を使いたい場合は"
          "その箇所をお知らせください」とのご指示、承知しました。</b>"
          "<b>その必要が生じた際にご連絡します。</b>"],
]

# 1-2. 率のカラムの型変更
RATE_TYPE_ROWS = [
    ["変更前", "変更後"],
    ["tinyint unsigned（整数）", "<b>double(4,1)</b>（小数第一位）"],
]

RATE_COL_ROWS = [
    ["テーブル", "列"],
    ["customer_summaries /<br/>facility_summaries",
     "operation_ratio / launch_ratio /<br/>clean_ratio / error_ratio（各4列）"],
    ["clean_status_histories /<br/>clean_statuses /<br/>clean_task_reports /<br/>"
     "robots /<br/>section_robot_statuses",
     "clean_ratio（各1列）"],
]

# 2. 接続先: あわせて確認させてください（第6回と同じ3件）
CONN_ROWS = [
    ["#", "確認したいこと", "補足"],
    ["1", "<b>接続元IPの制限はありますか</b>",
     "ある場合、<b>当方の接続元をお伝えします</b>のでお知らせください"],
    ["2", "<b>SSL/TLS 接続は必須でしょうか</b>",
     "必須の場合、<b>証明書の受け渡し方法</b>もお教えください"],
    ["3", "<b>接続先はステージング環境でしょうか、本番環境でしょうか</b>",
     "当方の扱い（参照のみ / 負荷のかけ方）を合わせたいためです"],
]

# 2. 当方の準備状況
PREP_ROWS = [
    ["#", "済んでいること"],
    ["1", "<b>いただいたDDLからローカルにDBを作成し、"
          "実DDLと列レベルで完全一致させました</b>"
          "（差分0件を機械的に確認済み）"],
    ["2", "<b>prisma db pull でスキーマを取り込み済みです</b>"
          "（今回の23列の型変更も反映済み）"],
    ["3", "<b>施設マスタ・スケジュール・D-Secログ・D-Secアラート・"
          "清掃詳細ログの5画面が、"
          "そのローカルDB経由で動くところまで実装済みです</b>"],
]

# 3. DDL の未反映項目（4件）
# B-1（索引・外部キー）と A-1（主キー）は当方の実装方針で必要が無くなったため
# 依頼から外し、取り下げの経緯を第1部 1-5 に移した（改訂2）。
DDL_ROWS = [
    ["#", "お願いしていた内容", "<b>09-09版の実測</b>"],
    ["A-2", "履歴テーブルの id を<br/>AUTO_INCREMENT で採番",
     "clean_status_histories.id /<br/>clean_timetable_histories.id とも<br/>"
     "bigint(20) unsigned NOT NULL。<br/>AUTO_INCREMENT 無し"],
    ["A-3b", "facility_contacts.email を<br/>varchar(100) に",
     "varchar(255) DEFAULT NULL のまま<br/>"
     "（contact_type は tinyint(3) unsigned<br/>で反映済みです）"],
    ["B-2", "robot_task_versions.clean_schedule_id<br/>にインデックス",
     "KEY は<br/>robot_task_versions_robot_id_foreign<br/>（robot_id）のみ。<br/>"
     "clean_schedule_id のインデックスは0件"],
    ["B-3b", "security_status_histories.reported_at<br/>に DEFAULT",
     "timestamp NOT NULL、<br/>DEFAULT なしのまま"],
]

# 3-1. 4件の性質（設計判断ではなく具体的なズレ）
GAP_ROWS = [
    ["#", "食い違いの中身", "補足"],
    ["A-2", "<b>採番の仕組み</b>",
     "id に AUTO_INCREMENT が無く、<br/>"
     "<b>誰が採番するのかが決まっていない状態</b>です"],
    ["A-3b", "<b>桁数</b>",
     "「email は varchar(100) とします」と<br/>"
     "ご回答いただいた桁数が、<br/>varchar(255) のままです"],
    ["B-2", "<b>索引の有無</b>",
     "<b>定義書で fk と記載されている列</b>ですが、<br/>"
     "実DDLには外部キーも索引もありません"],
    ["B-3b", "<b>DEFAULT の有無</b>",
     "timestamp NOT NULL に<br/>DEFAULT 句がありません"],
]

# 4. precheck
PRECHECK_ASK_ROWS = [
    ["#", "内容"],
    ["1", "<b>この理解で合っていますでしょうか</b>"],
    ["2", "<b>いつ頃反映される見込みでしょうか</b>"],
    ["3", "<b>既存のデータはどうなりますでしょうか</b> — "
          "1 / 2 のまま残るのか、60 / 30 に更新されるのか"],
    ["4", "<b>新旧の値が混在する期間はありますでしょうか</b>"],
    ["5", "<b>0 以外に予約値はありますでしょうか</b>"],
]

# 5. 面積の型変更
AREA_TYPE_ROWS = [
    ["変更前", "変更後"],
    ["decimal(12,6) unsigned",
     "<b>double(12,6)</b>（customer_summaries /<br/>"
     "facility_summaries は double(14,6)）"],
]

AREA_COL_ROWS = [
    ["テーブル", "列"],
    ["clean_status_histories /<br/>clean_statuses /<br/>clean_task_reports",
     "task_area / clean_area"],
    ["customer_summaries /<br/>facility_summaries", "task_area / clean_area"],
]

AREA_HARM_ROWS = [
    ["#", "理由"],
    ["1", "<b>viewer に面積を表示する箇所は1つもありません</b>"
          "（画面に出るのは % だけです）"],
    ["2", "<b>面積は完了率の計算材料としてのみ使う想定でしたが、"
          "率を小数でいただけるようになったため、"
          "当方で計算し直す必要も無くなりました</b>"],
]

# おわりに: 優先順（3件）
PRIO_ROWS = [
    ["順", "項目", "理由"],
    ["<b>1</b>", "<b>接続先<br/>（ホスト名・ポート）</b>",
     "<b>アカウントはいただいておりますので、これだけで接続できます</b>。"
     "9月中のデモを実データで行えるかがここで決まります"],
    ["2", "precheck の値の範囲と<br/>反映時期",
     "新旧混在の期間があるかどうかを特に知りたいです"],
    ["3", "DDL の未反映4件<br/>（A-2 / A-3b / B-2 / B-3b）",
     "お手すきの折で結構です。<br/>当方の開発は止まっていません"],
]


def build():
    doc = BaseDocTemplate(
        OUT, pagesize=A4,
        leftMargin=18 * mm, rightMargin=18 * mm,
        topMargin=18 * mm, bottomMargin=20 * mm,
        title="D-Watch 確認事項（第7回）",
        author="DFA Robotics",
        subject="09-09版DDLの照合結果のご共有と、接続先のご連絡のお願い",
    )
    frame = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id="f")
    doc.addPageTemplates([PageTemplate(id="p", frames=[frame], onPage=on_page)])

    W = doc.width
    CW = W - 6 * mm
    story = []

    # ---------------------------------------------------------- 表題と前文
    story.append(Paragraph("確認事項（第7回）", S_TITLE))
    story.append(Paragraph(
        "宛先: TeamWorks（DB開発側）　作成日: 2026-09-10　"
        "作成者: DFA Robotics", S_SUB))
    story.append(Spacer(1, 9))

    story.append(Paragraph(
        "<b>最新のDDL（clean_watch_20260909.sql）をお送りいただき、"
        "ありがとうございました。</b>"
        "（2026-09-09 受領。第6回でお願いしていた項目です）", S_BODY))
    story.append(Paragraph(
        "また、<b>率のカラムを小数第一位まで持つ形（double(4,1)）に"
        "変更いただき、ありがとうございました。</b>"
        "<b>当方からお願いしていた内容が、そのまま反映されておりました。</b>"
        "これで当方の判定処理が正しく行えるようになります（詳細は第1部）。",
        S_BODY))
    story.append(Paragraph("本書は次の5部構成です。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(SUMMARY_ROWS,
                       [46 * mm, W - 74 * mm, 28 * mm],
                       [S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>今回は、お伺いすることを3件に絞りました。</b>"
        "（<b>接続先 / DDL の未反映4件 / precheck の値域</b>）"
        "<b>お伺いする件数が少ないほど、"
        "早くお返事をいただけると考えたためです。</b>"
        "<b>第1部と第5部は当方からの共有ですので、回答は不要です。</b>",
        S_BODY))

    # ---------------------------------------------------------- 1. 照合結果
    story.append(CondPageBreak(60 * mm))
    story.append(Paragraph("1. 照合結果のご共有（回答不要です）", S_H2))
    story.append(Paragraph(
        "第6回で「<b>受領しましたら、その日のうちに照合してご報告します</b>」と"
        "お約束していた件です。"
        "<b>2026-09-09 に受領し、当日中に照合を完了しました。</b>", S_BODY))

    story.append(Paragraph("1-1. 照合方法", S_H3))
    story.append(Paragraph(
        "<b>ローカルの MySQL に読み込み、09-07版・09-02版と "
        "information_schema で機械的に差分を取りました。</b>"
        "目視ではなく、列名・型の両方を双方向で突き合わせています。", S_BODY))

    story.append(Paragraph(
        "1-2. 率のカラムが小数第一位になっていました（13列）", S_H3))
    story.append(Spacer(1, 2))
    story.append(table(RATE_TYPE_ROWS, [56 * mm, W - 56 * mm],
                       [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    # 5行のセルがあるので、導入文ごと表が割れないよう高さを確保してから置く
    story.append(CondPageBreak(50 * mm))
    story.append(Paragraph("対象は <b>13列</b> です。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(RATE_COL_ROWS, [58 * mm, W - 58 * mm],
                       [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>これで当方の判定が正しく行えるようになりました。"
        "ありがとうございます。</b>", S_BODY))
    story.append(Paragraph(
        "当方は清掃完了率から4段階（◎○△×）の判定を行っており、"
        "<b>その境界値は 90.0 / 90.1 / 70.0 / 70.1 / 10.0 / 9.9 と、"
        "すべて小数第一位までです。</b>", S_BODY))
    story.append(Paragraph(
        "整数で格納されていると、"
        "<b>例えば 89.6% が 90 に丸められて格納され、"
        "本来「○」であるべきものが「◎」になってしまう</b>"
        "という問題がありました。"
        "<b>今回の変更で、この問題は解消しました。</b>", S_BODY))

    story.append(Paragraph("1-3. テーブル・列の増減はありませんでした", S_H3))
    story.append(Paragraph(
        "<b>CREATE TABLE は 39 で、09-07版・09-02版と同数です。</b>"
        "テーブル名の集合も完全に一致しました。"
        "<b>列の増減もありません。</b>"
        "今回の変更は型（と一部の DEFAULT）だけでした。", S_BODY))
    story.append(Paragraph(
        "なお、面積のカラムも decimal から double に変わっておりましたので、"
        "この点だけ第5部で申し添えます"
        "（<b>当方に実害はありません</b>）。", S_BODY))

    # 1-4. MariaDB の件（送付前にご回答をいただけたので、質問から共有に変えた）
    story.append(CondPageBreak(70 * mm))
    story.append(Paragraph(
        "1-4. 本番が MariaDB であることをお教えいただき、"
        "ありがとうございました", S_H3))
    story.append(Paragraph(
        "<b>本書をお送りする前に、本番サーバが MariaDB であることを"
        "お教えいただきました。ありがとうございます。</b>", S_BODY))
    story.append(Paragraph(
        "（09-09版のダンプヘッダが 5.5.5-10.11.13-MariaDB-log に"
        "なっており、09-07版までの MySQL 8.0.41 と異なっておりましたので、"
        "お伺いしようとしていた件です。"
        "<b>先にご回答いただけましたので、本書からは質問を取り下げました。</b>）",
        S_BODY))
    story.append(Paragraph("当方の対応状況を申し添えます。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(MARIA_ROWS, [10 * mm, W - 10 * mm],
                       [S_CODE, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "なお、<b>当方のローカルの検証環境は MySQL です</b>ので、"
        "<b>今後もし生の SQL を書く場合は、"
        "MySQL と MariaDB の双方で通る書き方に注意いたします。</b>", S_BODY))

    # 1-5. B-1 / A-1 の取り下げ（改訂2）
    story.append(CondPageBreak(75 * mm))
    story.append(Paragraph(
        "1-5. これまでお願いしていた2件（外部キー／索引・主キー）を、"
        "今回の依頼から外します", S_H3))
    story.append(Paragraph(
        "第5回・第6回でお願いしておりました "
        "<b>B-1（clean_timetable_id の外部キー／索引）</b> と "
        "<b>A-1（robot_error_reports の主キー）</b> について、"
        "<b>当方の実装が固まり、いずれも当方側で支障が無いことが"
        "はっきりしました。</b>"
        "<b>今回の依頼から外させてください。</b>", S_BODY))

    story.append(Paragraph(
        "B-1（clean_timetable_id の外部キー・索引）", S_H4))
    story.append(Paragraph(
        "<b>当方は、これらのテーブルを全件取得し、"
        "アプリ側で突き合わせる実装にしました。</b>", S_LEAD))
    story.extend(bull([
        "clean_timetables / clean_status_histories /<br/>"
        "robot_error_reports を、それぞれ "
        "<b>WHERE 句なしの全件取得</b>で読み出しています",
        "読み出したあと、clean_timetable_id をキーにした対応表を"
        "メモリ上に作り、そこで結合しています",
        "<b>つまり、clean_timetable_id で「検索」をしておりません。</b>"
        "<b>索引は WHERE 句で絞り込むときに効くものですので、"
        "この実装では使われません。</b>",
    ]))
    story.append(Spacer(1, 2))
    story.append(Paragraph(
        "<b>この実装はすでに完成しており、外部キーが無いまま"
        "清掃ログの画面が動いております。</b>", S_BODY))
    story.append(Paragraph(
        "また、<b>どの索引が必要かは、実際のクエリの形と、"
        "実データの量・分布で決まります。</b>"
        "<b>当方はまだ実データを1行も見ておらず、"
        "絞り込みのクエリも書いておりません。</b>"
        "<b>この状態で特定の索引を名指しでお願いするのは、"
        "当方の根拠が薄いと考え直しました。</b>", S_BODY))
    story.append(Paragraph(
        "<b>したがって、現時点で支障はありません。</b>"
        "<b>将来クエリの形が変わって索引が必要になりましたら、"
        "その時点で具体的なクエリを添えて、"
        "あらためてご相談します。</b>", S_BODY))
    story.append(Paragraph(
        "過去の確認書では外部キーの追加をお願いしておりましたが、"
        "<b>当方の実装方針が定まった結果、当方の見立てが変わりました。</b>"
        "お手数をおかけしました。", S_BODY))

    story.append(CondPageBreak(40 * mm))
    story.append(Paragraph(
        "A-1（robot_error_reports の主キー）", S_H4))
    story.append(Paragraph(
        "error_id を識別子として使う方針は第4回のご回答でいただいており、"
        "当方はその前提で実装しております。", S_BODY))
    story.append(Paragraph(
        "<b>主キーが無くても当方の実装は動いております。"
        "error_id に UNIQUE がありますので、一意性は担保されています。</b>"
        "<b>当方の実装で支障が無い以上、"
        "DDL の変更をお願いする根拠がありませんので、"
        "こちらも依頼から外します。</b>", S_BODY))

    # ---------------------------------------------------------- 2. 接続先
    story.append(CondPageBreak(75 * mm))
    story.append(Paragraph(
        "2. 接続先（ホスト名・ポート）をお教えいただけますでしょうか — 最優先",
        S_H2))
    story.append(Paragraph(
        "<b>第6回でもお願いした件で、恐れ入りますが繰り返しになります。</b>",
        S_BODY))
    story.append(Paragraph(
        "<b>参照用アカウントは 2026-09-08 にいただいておりますので、"
        "あとは接続先だけです。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(callout(
        "接続先のホスト名（または IP アドレス）と、ポート番号", CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph("をお教えいただければ接続できます。", S_BODY))

    story.append(Paragraph("あわせて確認させてください", S_H3))
    story.append(Spacer(1, 3))
    story.append(table(CONN_ROWS, [10 * mm, 60 * mm, W - 70 * mm],
                       [S_CODE, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))

    # 見出しと2つの箇条書きが改ページで割れないように高さを確保する
    story.append(CondPageBreak(28 * mm))
    story.append(Paragraph("なぜ急いでお願いしているか", S_H3))
    story.extend(bull([
        "<b>9月中に PPIH 様向けにデモを実施する可能性があります。</b>",
        "<b>実データでお見せできるかどうかが、この接続情報で決まります。</b>",
    ]))

    story.append(CondPageBreak(60 * mm))
    story.append(Paragraph("当方の準備状況", S_H3))
    story.append(Paragraph(
        "<b>接続情報さえいただければ、すぐに使える状態になっております。</b>",
        S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(PREP_ROWS, [10 * mm, W - 10 * mm],
                       [S_CODE, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>接続先をいただければ、接続文字列を差し替えるだけです。</b>", S_BODY))
    story.append(Paragraph(
        "また第6回でお伝えしたとおり、接続でき次第、"
        "error_type の値の分布・robots.status の実出現値・"
        "週次サマリの week の値域など "
        "<b>当方で数えられるものは当方で確認します</b> ので、"
        "<b>その分だけ貴社への照会が減ります。</b>", S_BODY))

    # ---------------------------------------------------------- 3. 未反映項目
    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "3. DDL の未反映項目（4件）を、"
        "お手すきの折にご対応いただけますでしょうか", S_H2))
    story.append(Paragraph(
        "<b>お忙しいところ恐縮です。3回目のお願いになります。</b>"
        "第5回・第6回でお願いしていた項目のうち、"
        "<b>4項目が 09-09版でも未反映でした。</b>", S_BODY))
    story.append(Paragraph(
        "<b>今回のDDLは率と面積の型変更が主眼だったかと存じますので、"
        "これらは単に今回のスコープに入っていなかっただけかと思っております。</b>"
        "状況だけ共有させてください。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(DDL_ROWS, [12 * mm, 68 * mm, W - 80 * mm],
                       [S_CODE, S_CODE, S_CELL], head_color=WN))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "3-1. この4件は、当方の設計上のお願いではなく、具体的なズレです",
        S_H3))
    story.append(Paragraph(
        "1-5 で取り下げた2件（外部キー／索引・主キー）は、"
        "<b>「DBの設計をどうするか」という判断を伴うお願い</b>でした。"
        "<b>当方の実装方針が固まり、必要がなくなりましたので"
        "取り下げました。</b>", S_BODY))
    story.append(Paragraph(
        "<b>一方、この4件は性質が違います。</b>"
        "<b>いずれも、採番・桁数・DEFAULT・索引の有無という、"
        "具体的で機械的な食い違いです。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(GAP_ROWS, [12 * mm, 30 * mm, W - 42 * mm],
                       [S_CODE, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>いずれも「どちらが正なのか」が当方には判断できない箇所です。</b>"
        "<b>意図的なものでしたら、"
        "その旨だけお知らせいただければ結構です。</b>", S_BODY))

    story.append(Paragraph("3-2. お手すきの折で結構です", S_H3))
    story.append(Paragraph(
        "<b>いずれも当方の開発を止めておりません。</b>"
        "<b>お手すきの折に、まとめてご対応いただければ十分です。</b>", S_BODY))
    story.append(Spacer(1, 2))
    story.append(quote(
        "<b>急かす意図はありません。</b>状況が分かれば大丈夫です。<br/>"
        "<b>次回まとまった段階でDDLをいただければ、"
        "同じように当日中に照合してご報告します。</b>", CW))

    # ---------------------------------------------------------- 4. precheck
    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "4. clean_timetables.precheck に入る値の範囲を"
        "お教えいただけますでしょうか", S_H2))

    story.append(Paragraph("4-1. 現状の理解", S_H3))
    story.extend(bull([
        "clean_timetables.precheck は現行 "
        "<b>tinyint unsigned / 既定値 0</b> です（09-09版でも変化なし）",
        "<b>DFA 側で「0 = 本清掃 / 60 = 60分前 / 30 = 30分前」のように、"
        "分前の数値をそのまま入れる方向でご検討いただいている</b> "
        "と伺っております",
        "設定値によっては <b>20 なら20分前</b> のように、"
        "任意の分数が入りうるとも伺っております",
    ]))

    story.append(Paragraph("4-2. お伺いしたいこと", S_H3))
    story.append(Spacer(1, 3))
    story.append(table(PRECHECK_ASK_ROWS, [10 * mm, W - 10 * mm],
                       [S_CODE, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>特に4番をお伺いしたいです。</b>"
        "<b>混在すると、当方は 1 を「1分前」と読み違えてしまうためです。</b>"
        "<b>値だけを見て新旧を判別する方法がありませんので、"
        "期間が分かれば当方で切り替え時期を合わせられます。</b>", S_BODY))

    story.append(CondPageBreak(45 * mm))
    story.append(Paragraph("4-3. なぜお伺いするか", S_H3))
    story.append(Paragraph(
        "<b>アラート画面で「60分前の異常」「30分前の異常」を出し分けるのに、"
        "この値を使います。</b>", S_BODY))
    story.append(Paragraph(
        "<b>値の意味を取り違えると、"
        "現場への連絡タイミングの判断を誤ります。</b>"
        "「30分前のチェックで異常が出た」のか「60分前で出た」のかによって、"
        "現場に間に合うかどうかが変わるためです。", S_BODY))
    story.append(Paragraph(
        "なお、<b>DDL からは値の意味が読めませんでした</b>"
        "（カラムコメントが空のため）。"
        "<b>型は tinyint unsigned（0〜255）ですので、"
        "60 も 30 も 20 も、型の変更なしで入ります。</b>"
        "つまり <b>値の意味の変更は DDL には現れません</b> ので、"
        "お伺いする次第です。", S_BODY))

    # ---------------------------------------------------------- 5. 面積
    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "5. 面積のカラムの型変更について（共有のみ。回答は不要です）", S_H2))
    story.append(Paragraph(
        "<b>これは要望でも質問でもなく、事実の共有だけです。</b>", S_BODY))

    story.append(Paragraph("5-1. 実測", S_H3))
    story.append(Paragraph(
        "09-09版で、面積のカラムも型が変わっておりました（<b>10列</b>）。",
        S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(AREA_TYPE_ROWS, [54 * mm, W - 54 * mm],
                       [S_CELL, S_CELL]))
    story.append(Spacer(1, 5))
    story.append(CondPageBreak(38 * mm))
    story.append(table(AREA_COL_ROWS, [58 * mm, W - 58 * mm],
                       [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))

    story.append(Paragraph("5-2. 当方に実害はありません", S_H3))
    story.append(Paragraph(
        "<b>この変更で当方が困ることはありません。</b>"
        "理由は2つです。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(AREA_HARM_ROWS, [10 * mm, W - 10 * mm],
                       [S_CODE, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>したがって、お戻しいただく必要はありません。</b>", S_BODY))

    story.append(Paragraph("5-3. 意図の確認は今回は見送ります", S_H3))
    story.append(Paragraph(
        "当初は「率の小数化に伴う変更でしょうか」と"
        "お伺いしようと考えておりましたが、"
        "<b>上記のとおり当方に実害がありませんので、"
        "今回は質問といたしません。</b>"
        "<b>お伺いする件数を絞り、"
        "早くお返事をいただきたいためです。</b>", S_BODY))
    story.append(Paragraph(
        "もし何か意図がおありでしたら、"
        "<b>次回ついでの折にお聞かせいただければ十分です。</b>", S_BODY))

    # ---------------------------------------------------------- 末尾
    story.append(CondPageBreak(75 * mm))
    story.append(Paragraph("おわりに", S_H2))
    story.append(Paragraph(
        "<b>今回は、お伺いすることを3件に絞りました。</b>"
        "<b>お伺いする件数が少ないほど、"
        "早くお返事をいただけると考えたためです。</b>", S_BODY))
    story.append(Paragraph(
        "優先度をつけるとすれば次の順です。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(PRIO_ROWS, [10 * mm, 50 * mm, W - 60 * mm],
                       [S_CODE, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>1（接続先）が最優先です。</b>"
        "<b>これだけ先にいただければ、当方で確認できることが一気に増え、"
        "その分だけ貴社への照会が減ります。</b>", S_BODY))
    story.append(Paragraph(
        "なお、これまでお願いしておりました外部キー／索引（B-1）と"
        "主キー（A-1）は、"
        "<b>当方の実装方針が固まり必要がなくなりましたので、"
        "今回の依頼から外しました</b>（1-5 に経緯を記しました）。", S_BODY))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "率のカラムの小数化、そして本番が MariaDB であることのご教示、"
        "あらためてありがとうございました。", S_BODY))
    story.append(Paragraph("引き続きよろしくお願いします。", S_NOTE))

    doc.build(story)
    print("生成:", OUT)


if __name__ == "__main__":
    build()
