# -*- coding: utf-8 -*-
"""
実DDL照合結果と確認事項を PDF にする。

前回 TeamWorks へ送付した「テーブル定義書レビュー結果」
(make_schema_review_pdf.py) の体裁をそのまま踏襲した続編。
A4 縦 / reportlab / 日本語フォント埋め込み / ヘッダーとページ番号。

フォントは BIZ UDGothic（日本語フォント）を埋め込む。
中国語向けフォントは漢字の字形が異なるため使わない。
フォールバックにも混ぜないこと。

BIZ UDGothic は等幅（半角は 0.5em 固定）なので、SQL の枠にもこのフォントを使う。
コードブロック内の空白は &nbsp; に置換して桁を保持する。

原稿 (docs/handoff/TeamWorks実DDL照合結果_第1回_20260903.md) の
文言・件数・表の内容は変更していない。
原稿中の指示用見出し（「サマリ表（冒頭に置く）」等）は見出しとして出力せず、
内容だけを該当位置に配置している。
"""
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, "D-Watch_実DDL照合結果_20260903.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")   # A 区分の強調色（前回 PDF と同一）
WN = colors.HexColor("#b45309")   # B 区分の強調色（前回 PDF と同一）

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_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)


def P(t, s=S_CELL):
    return Paragraph(t, s)


def on_page(canvas, doc):
    """全ページ共通のヘッダー罫線・フッター・ページ番号。前回の 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 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]


# ---------------------------------------------------------------- 表データ
SUMMARY_ROWS = [
    ["区分", "件数", "性質"],
    ["A. 合意が必要", "3件", "返答が無いと実装を決められない"],
    ["B. 情報共有", "3件", "返答は必須でない。DB側でしか対処できないものを含む"],
    ["C. 仕様確認", "1件", "以前の合意事項の解釈"],
]

# A-3: ご回答と実DDL の食い違い
A3_ROWS = [
    ["カラム", "ご回答", "実DDL"],
    ["customer_contacts.contact_type", "unsigned tinyint", "<b>bigint unsigned</b>"],
    ["customer_contacts.email", "varchar(100)", "varchar(100)（一致）"],
    ["facility_contacts.contact_type", "unsigned tinyint", "tinyint unsigned（一致）"],
    ["facility_contacts.email", "varchar(100)", "<b>varchar(255)</b>"],
]

# B-1: 欠けている外部キー
B1_ROWS = [
    ["テーブル", "カラム", "定義書上の参照先"],
    ["clean_statuses", "clean_timetable_id", "clean_timetables(id)"],
    ["clean_status_histories", "clean_timetable_id", "clean_timetables(id)"],
    ["robot_task_versions", "clean_schedule_id", "clean_schedules(id)"],
]

# B-3: DEFAULT 句が無い timestamp
B3_ROWS = [
    ["テーブル", "カラム"],
    ["clean_task_reports", "created_at, updated_at"],
    ["security_status_histories", "reported_at"],
]

# 参考: 照合により解決した事項
R_ROWS = [
    ["保留していた事項", "DDL での確認結果"],
    ["対応日時のカラム名",
     "<b>responded_at</b>（正しい綴り）。定義書の responsed_at は誤記と確定"],
    ["control_reports のシリアル番号",
     "<b>serial_number</b>。定義書の sireal_number は誤記と確定"],
    ["clean_status の -1 問題",
     "実DDL は符号あり tinyint。<b>定義書の型表記が誤りで、備考が正しかった</b>"],
    ["makers テーブル",
     "DDL に存在せず、devices からの外部キーも無し。<b>ご回答どおり</b>"],
    ["controls.facility_id の外部キー", "<b>存在を確認</b>。ご回答どおり"],
    ["clean_timetables.precheck の既定値",
     "<b>0</b>（本清掃）。定義書の 9 は誤りと確定"],
    ["clean_link_schedules",
     "<b>不要テーブルとご確認済み</b>。link_schedules を参照します"],
    ["section_robot_statuses.indicatived_at",
     "<b>綴り違いではなく、列自体が廃止</b>されていました"],
]


def build():
    doc = BaseDocTemplate(
        OUT, pagesize=A4,
        leftMargin=18 * mm, rightMargin=18 * mm,
        topMargin=18 * mm, bottomMargin=20 * mm,
        title="D-Watch 実DDL照合結果と確認事項",
        author="DFA Robotics",
        subject="実DDL（clean_watch_20260902.sql）との照合結果と確認事項",
    )
    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("実DDL照合結果と確認事項", S_TITLE))
    # 1 行に収まる長さで確定させている（9.5pt で本文幅の 97%）
    story.append(Paragraph(
        "宛先: TeamWorks（DB開発側）　対象: clean_watch_20260902.sql　"
        "作成日: 2026-09-03　作成者: DFA Robotics", S_SUB))
    story.append(Spacer(1, 9))

    story.append(Paragraph(
        "2026-09-02 付でご提供いただいた clean_watch_20260902.sql（CREATE TABLE 39個）を、"
        "こちらで手書きしていた Prisma スキーマと機械的に照合しました。", S_BODY))
    story.append(Paragraph(
        "照合は独立した2つの方法で行い、結果が一致することを確認しています。", S_LEAD))
    story.extend(num([
        "DDL からローカルに実データベースを構築し、prisma db pull で生成したスキーマと比較",
        "DDL を直接パースしてカラム定義を抽出し、比較",
    ]))
    story.append(Spacer(1, 3))
    story.append(Paragraph(
        "差分は49件でした。そのうち <b>42件はこちらで実DDLに合わせるだけで完結</b>します。"
        "本書には、<b>こちらだけでは判断できない3件</b>と、<b>共有しておきたい3件</b>、"
        "および<b>仕様の確認1件</b>を記載しています。", S_BODY))
    story.append(Spacer(1, 4))

    # ---------------------------------------------------------- サマリ表
    story.append(table(SUMMARY_ROWS, [30 * mm, 14 * mm, W - 44 * mm],
                       [S_CELL, S_CODE, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "こちらで完結する42件については本書に記載していません。", S_NOTE))

    # ---------------------------------------------------------- A
    story.append(Paragraph("A. 合意が必要（3件）", S_H2))

    story.append(Paragraph(
        "A-1　robot_error_reports の主キーと id の意味", S_H3))
    story.append(Paragraph("<b>最も影響が大きい項目です。</b>", S_BODY))
    story.append(Paragraph("実DDL の状態:", S_LEAD))
    story.append(code([
        "`id`       varchar(50) NOT NULL,   -- 主キーでも UNIQUE でもない。制約なし",
        "`error_id` varchar(50) NOT NULL,",
        "UNIQUE KEY `robot_error_reports_report_id_unique` (`error_id`)",
    ], CW))
    story.append(Spacer(1, 5))
    story.append(Paragraph(
        "PRIMARY KEY が定義されておらず、一意制約は error_id のみです。", S_BODY))
    story.append(Paragraph(
        "一方こちらの API 契約と型定義は次のようになっています。", S_LEAD))
    story.append(code([
        "PrecheckItem.id : number",
        "PATCH /prechecks/{id}/status",
    ], CW))
    story.append(Spacer(1, 5))
    story.append(Paragraph(
        "具体的な帰結です。アラート一覧で担当者が「対応中」を押すと、画面は id を送ります。"
        "しかし、", S_LEAD))
    story.extend(bull([
        "id は一意でないため、その値で1行を特定できません",
        "id は文字列なので、number として扱うと不一致が起きます",
        "更新できるのは error_id だけです",
    ]))
    story.append(Spacer(1, 3))
    story.append(Paragraph(
        "<b>このため対応状況の更新（PATCH）が設計として成立しません。</b>", S_BODY))
    story.append(Paragraph("伺いたいこと:", S_LEAD))
    story.extend(num([
        "id に一意制約を付ける予定はありますか。それとも error_id が正式なキーですか",
        "こちらの API が画面に返すべき ID は id と error_id のどちらですか",
        "id が文字列である前提でよろしいですか（こちらの型を number から string に変更します）",
    ]))

    story.append(CondPageBreak(45 * mm))
    story.append(Paragraph("A-2　履歴テーブルの id は誰が採番するのか", S_H3))
    story.append(Paragraph("clean_status_histories の状態:", S_LEAD))
    story.append(code([
        "`id` bigint unsigned NOT NULL,   -- AUTO_INCREMENT ではない",
        "UNIQUE KEY `clean_status_histories_id_unique` (`id`)",
    ], CW))
    story.append(Spacer(1, 5))
    story.append(Paragraph(
        "PRIMARY KEY が無く、AUTO_INCREMENT も付いていません。"
        "一方で UNIQUE 制約はあります。", S_BODY))
    story.append(Paragraph(
        "これが元テーブル clean_statuses.id を引き継ぐ設計だとすると、"
        "<b>1つの清掃状態につき履歴を1件しか作れません。</b>"
        "履歴テーブルとしては不自然に見えるため、意図を確認したいです。", S_BODY))
    story.append(Paragraph(
        "clean_timetable_histories.id も同様に AUTO_INCREMENT がありません。", S_BODY))
    story.append(Paragraph("伺いたいこと:", S_LEAD))
    story.extend(num([
        "これらの id は誰が採番しますか（アプリ側で連番を振る想定でしょうか）",
        "元テーブルの id を引き継ぐ設計ですか。その場合、同じ清掃状態の履歴が"
        "複数回作られることはない前提でよろしいですか",
    ]))
    story.append(Spacer(1, 3))
    story.append(Paragraph(
        "こちらが検証用のダミーデータを作る際に、採番方法が決まらないと投入できません。",
        S_BODY))

    story.append(CondPageBreak(50 * mm))
    story.append(Paragraph("A-3　先の回答（contact 系の型）はいつ反映されますか", S_H3))
    story.append(Paragraph(
        "先の確認事項 B-6 について、次のようにご回答いただきました。", S_LEAD))
    story.append(quote(
        "typo です。contact_type は unsigned tinyint とします。"
        "email は varchar(100) とします。", CW))
    story.append(Spacer(1, 5))
    story.append(Paragraph(
        "いただいた DDL（2026-09-02 付）には、まだ反映されていないようです。", S_LEAD))
    # 長いカラム名が折り返さない幅を確保する（52mm）
    story.append(table(A3_ROWS, [52 * mm, 32 * mm, W - 84 * mm],
                       [S_CODE, S_CODE, S_CODE]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "顧客側と施設側で型が食い違った状態が残っています。", S_BODY))
    story.append(Paragraph("伺いたいこと:", S_LEAD))
    story.extend(num([
        "当面は現行の DDL に合わせて実装してよろしいですか"
        "（後で変更が入れば prisma db pull の差分に出るため追随できます）",
        "変更を入れる時期の目安があればお知らせください",
    ]))
    story.append(Spacer(1, 3))
    story.append(Paragraph(
        "なお customer_contacts.contact_type が bigint の場合、"
        "こちらでは JavaScript の bigint 型として受け取ることになり、"
        "JSON へ変換する際に明示的な数値変換が必要になります（実装側で対応します）。",
        S_BODY))

    # ---------------------------------------------------------- B
    # B 区分の見出しだけがページ末に取り残されないよう、B-1 の表まで含めた高さで判定する
    story.append(CondPageBreak(75 * mm))
    story.append(Paragraph("B. 情報共有（3件）", S_H2))
    story.append(Paragraph(
        "返答は必須ではありません。こちらは現状のまま実装を進められます。", S_NOTE))

    story.append(Paragraph("B-1　外部キーが3本欠けています", S_H3))
    story.append(Paragraph(
        "定義書では fk と記載されているものの、実DDL に制約が見当たりませんでした。",
        S_LEAD))
    story.append(table(B1_ROWS, [48 * mm, 42 * mm, W - 90 * mm],
                       [S_CODE, S_CODE, S_CODE], WN))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "意図的なものか、付け忘れかをご確認いただければ幸いです。"
        "付け忘れであれば DB 側で追加いただくのが適切と考えます。"
        "意図的であれば、こちらは参照先が存在しない行（孤児行）を想定した実装にします。",
        S_BODY))
    story.append(Paragraph(
        "<b>返答をお待ちせず、孤児行を想定した実装で進めます。</b>", S_BODY))

    story.append(CondPageBreak(35 * mm))
    story.append(Paragraph(
        "B-2　インデックスが1つ欠けています（DB側でしか対処できません）", S_H3))
    story.append(Paragraph(
        "robot_task_versions.clean_schedule_id にインデックスがありません。"
        "外部キーも無いため、MySQL による自動作成も行われていません。", S_BODY))
    story.append(Paragraph(
        "この列で絞り込む検索が全件走査になります。"
        "300施設分のタスク履歴が積み上がるテーブルのため、"
        "運用が進むにつれて表示が遅くなることが見込まれます。", S_BODY))
    story.append(Paragraph(
        "<b>こちら側に回避手段がないため、DB 側でのご対応をお願いしたい項目です。</b>",
        S_BODY))

    story.append(CondPageBreak(35 * mm))
    story.append(Paragraph("B-3　timestamp NOT NULL に DEFAULT 句がありません", S_H3))
    story.append(table(B3_ROWS, [55 * mm, W - 55 * mm], [S_CODE, S_CODE]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "いずれも timestamp NOT NULL ですが DEFAULT 句がなく、"
        "INSERT 時に明示的な値の指定が必要です。", S_BODY))
    story.append(Paragraph(
        "アプリ側で時刻を管理する設計であれば意図的と理解しますが、"
        "created_at に既定値が無いのは珍しい形のため念のため共有します。", S_BODY))
    story.append(Paragraph(
        "<b>こちらは参照のみのため実装への影響はありません。</b>", S_BODY))

    # ---------------------------------------------------------- C
    story.append(Paragraph("C. 仕様確認（1件）", S_H2))
    story.append(CondPageBreak(45 * mm))
    story.append(Paragraph(
        "C-1　アドバイス規則 R3 の条件に「出発失敗」を含みますか", S_H3))
    story.append(Paragraph(
        "こちらの実装規約では、行展開アドバイスの規則を次のように定めています。", S_LEAD))
    story.append(code([
        "R3 = 完了率が△以下の状態が2日以上連続",
        "R4 = 出発失敗 または △以下 の状態が3日以上連続",
    ], CW))
    story.append(Spacer(1, 5))
    story.append(Paragraph(
        "R3 と R4 で条件を書き分けていますが、実装は両方とも"
        "「連続問題日数」という1つの値を見ており、区別できません。", S_BODY))
    story.append(Paragraph(
        "さらに連続問題日数の定義が「△以下 <b>または</b> 出発失敗」であるため、"
        "<b>現状では2日連続の出発失敗で R3 が発火します。</b>", S_BODY))
    story.append(Paragraph("伺いたいこと:", S_LEAD))
    story.extend(num([
        "R3 の条件に出発失敗を含めるのが正しいですか",
        "それとも R3 は完了率の低下のみを対象とし、出発失敗は R4 だけの条件ですか",
    ]))
    story.append(Spacer(1, 3))
    story.append(Paragraph(
        "後者の場合、連続日数を「△以下のみ」と「出発失敗を含む」の2種類に分けて"
        "数える必要があるため、実装方針が変わります。", S_BODY))

    # ---------------------------------------------------------- 参考
    story.append(CondPageBreak(60 * mm))   # 見出しだけがページ末に残らないようにする
    story.append(Paragraph("参考: 照合により解決した事項", S_H2))
    story.append(Paragraph(
        "いただいた DDL により、こちらで保留していた判断が確定しました。"
        "あわせてご報告します。", S_NOTE))
    story.append(Spacer(1, 3))
    # 左列は section_robot_statuses.indicatived_at が折り返さない幅にする
    story.append(table(R_ROWS, [66 * mm, W - 66 * mm], [S_CODE, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "また、こちらが定義書から手書きしたスキーマは、"
        "28テーブル中12テーブルが実DDLと完全一致していました。"
        "残りの差分も、大半は定義書側の記述と実DDL の食い違いに起因するものでした。",
        S_BODY))

    # ---------------------------------------------------------- 末尾
    story.append(Spacer(1, 10))
    story.append(Paragraph(
        "本書の A（3件）についてご回答をいただければ、"
        "対応状況の更新機能とダミーデータによる検証に着手できます。"
        "B・C については実装を止めるものではありませんので、"
        "お手隙の際にご確認ください。", S_NOTE))

    doc.build(story)
    print("生成:", OUT)


if __name__ == "__main__":
    build()
