# -*- coding: utf-8 -*-
"""
TeamWorks 宛 確認事項（第4回）を PDF にする。

第1回〜第3回（make_ddl_review_pdf.py / make_teamworks_qa2_pdf.py /
make_teamworks_qa3_pdf.py）の体裁を
そのまま踏襲した続編。A4 縦 / reportlab / 日本語フォント埋め込み /
ヘッダー罫線とページ番号。

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

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

原稿は docs/handoff/TeamWorks確認事項_第4回_20260907.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, "DWatch_確認事項_第4回_20260907.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_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):
    """全ページ共通のヘッダー罫線・フッター・ページ番号。第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 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 = [
    ["区分", "件数", "性質"],
    ["1. 修正版DDLの照合結果", "7件中2件", "<b>5件がまだ入っていません。</b>状況をお知らせください"],
    ["2. 新しくお願いしたいこと", "2件", "<b>W-1 は集計の食い違いに直結します</b>"],
    ["3. 第3回からの継続", "2件", "まだ回答をいただいていないもの"],
]

# 1. 反映されていたもの
DONE_ROWS = [
    ["内容", "変化"],
    ["customer_contacts.contact_type", "bigint unsigned → <b>tinyint unsigned</b>"],
    ["clean_task_reports の created_at / updated_at",
     "<b>DEFAULT CURRENT_TIMESTAMP</b> を付与（updated_at は ON UPDATE 付き）"],
]

# 1. まだ入っていないもの
PENDING_ROWS = [
    ["#", "お願いしていた内容", "現在のDDL"],
    ["A-1", "error_id を主キーと位置付けて対応",
     "<b>PRIMARY KEY 無しのまま。</b>UNIQUE KEY は error_id のみ"],
    ["A-2", "履歴テーブルの id を<br/>auto_increment で採番",
     "clean_status_histories.id / clean_timetable_histories.id とも <b>AUTO_INCREMENT 無し</b>。"
     "ファイル全体の AUTO_INCREMENT 数が <b>50 で 09-02 版と同数</b>"],
    ["A-3b", "email を両テーブルとも<br/>varchar(100)",
     "<b>facility_contacts.email が varchar(255) のまま</b>"
     "（contact_type だけ直っています）"],
    ["B-1", "外部キー3本を追加",
     "<b>3本とも無し。</b>CONSTRAINT 総数が <b>48 で 09-02 版と同数</b>。"
     "制約名（clean_statuses_clean_timetable_id_foreign 等）を全文検索しても見つかりません"],
    ["B-2", "robot_task_versions.clean_schedule_id<br/>にインデックス",
     "KEY は robot_id のみ"],
    ["B-3b", "security_status_histories.reported_at<br/>に DEFAULT",
     "<b>timestamp NOT NULL のまま</b>"],
]

# W-1: 週の帰属の具体例
WEEK_ROWS = [
    ["週", "当方の扱い"],
    ["2026-08-23(日) 〜 08-29(土)", "8月の週"],
    ["<b>2026-08-30(日) 〜 09-05(土)</b>",
     "★<b>9月 第1週</b>★（8/30・8/31 を含むが 9月に数える）"],
    ["2026-09-06(日) 〜 09-12(土)", "9月 第2週"],
]

# W-2: 6状態と robots.status の対応
STATE_ROWS = [
    ["画面に出す値", "robots.status の対応"],
    ["待機中", "0 Standby"],
    ["充電中", "201 Charging Line / 202 Charging Pile"],
    ["セキュリティ有効", "security_statuses.status = 1 から判定"],
    ["<b>ハンドルアップ</b>", "<b>該当なし</b>"],
    ["<b>緊急停止</b>", "<b>該当なし</b>"],
    ["<b>オフライン</b>", "<b>該当なし</b>"],
]

# 3. 継続
CONT_ROWS = [
    ["#", "内容", "状態"],
    ["D-1", "link_schedules.intervals（VarChar(120)）の<b>書式</b>。"
            "実際に入っている値の例を1〜2件",
     "★<b>これが分からないと D-Secログの通信断判定が実装できません</b>★ 最優先でお願いします"],
    ["D-4", "PrecheckItem.id の切替対象は「id と error_id のどちらを使うか」だけで、"
            "<b>型は varchar のまま</b>という理解でよいか",
     "継続。数値型に変わる可能性があるなら、API の破壊的変更になるため事前に知りたいです"],
]


def build():
    doc = BaseDocTemplate(
        OUT, pagesize=A4,
        leftMargin=18 * mm, rightMargin=18 * mm,
        topMargin=18 * mm, bottomMargin=20 * mm,
        title="D-Watch 確認事項（第4回）",
        author="DFA Robotics",
        subject="修正版DDL（clean_watch_20260907.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("確認事項（第4回）", S_TITLE))
    story.append(Paragraph(
        "宛先: TeamWorks（DB開発側）　対象: clean_watch_20260907.sql　"
        "作成日: 2026-09-07　作成者: DFA Robotics", S_SUB))
    story.append(Spacer(1, 9))

    story.append(Paragraph(
        "修正版のDDLをありがとうございました。<b>受領したその日に照合しています。</b>", S_BODY))
    story.append(Paragraph("本書は次の3部構成です。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(SUMMARY_ROWS, [46 * mm, 18 * mm, W - 64 * mm],
                       [S_CELL, S_CODE, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(quote(
        "第3回でお伺いした D-3（セキュリティ解除の判定）は当方で解決しました。"
        "「制御盤の生死報告」と「セキュリティ解除の状態」は別の概念で、生死報告が来ない場合は"
        "サーバーから制御盤異常としてエラーが上がるが、セキュリティ解除はエラーにならず"
        "別の値で管理される、という整理です。security_statuses.status = 0 の継続から"
        "当方で導出します。回答は不要です。", CW))

    # ---------------------------------------------------------- 1
    story.append(Paragraph("1. 修正版DDLの照合結果", S_H2))
    story.append(Paragraph(
        "clean_watch_20260902.sql との差分は<b>14行</b>でした。"
        "テーブル数は 39 で変化ありません。", S_BODY))

    story.append(Paragraph("反映されていたもの（2件）— ありがとうございました", S_H3))
    story.append(table(DONE_ROWS, [62 * mm, W - 62 * mm], [S_CODE, S_CELL]))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(70 * mm))
    story.append(Paragraph("★まだ入っていないもの（5件）★", S_H3))
    story.append(table(PENDING_ROWS, [14 * mm, 64 * mm, W - 78 * mm],
                       [S_CODE, S_CELL, S_CELL], head_color=SUB))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "照合は<b>3つの方法</b>（09-02版との全体差分 / 各 CREATE TABLE ブロックからの項目抽出 / "
        "追加されるはずの制約名の全文検索）で行い、いずれも同じ結果でした。", S_BODY))
    story.append(Paragraph("伺いたいこと:", S_LEAD))
    story.extend(num([
        "<b>これらは「これから対応」という理解でよいでしょうか。</b>それとも"
        "<b>意図的に見送られた</b>ものがありますか"
        "（あれば、その旨だけ教えていただければこちらは現行DDLに合わせて実装します）",
        "対応いただける場合、<b>まとまった段階で改めてDDLをいただけますか。</b>目安の時期があれば",
    ]))
    story.append(Spacer(1, 3))
    story.append(quote(
        "急かす意図はありません。上記5件はいずれも当方の開発を止めていません。"
        "こちらは受領したDDLをそのまま prisma db pull で取り込む運用にしているため、"
        "状況が分かれば十分です。", CW))

    # ---------------------------------------------------------- 2
    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph("2. 新しくお願いしたいこと（2件）", S_H2))

    story.append(Paragraph("W-1 ★月跨ぎの週を、どちらの月に数えていますか★", S_H3))
    story.append(Paragraph(
        "<b>週次サマリを作成いただくのは TeamWorks 様側</b>という理解です"
        "（毎週日曜 6:00 / 前週 日〜土）。一方、進行中の週（当期）は当方が詳細テーブルから"
        "集計します。<b>両者で「週の帰属」が食い違うと、同じ週が別の月に集計されます。</b>", S_BODY))
    story.append(Paragraph("<b>当方は次のルールで実装します。</b>", S_LEAD))
    story.append(Spacer(1, 2))
    story.append(quote(
        "その月の1日を含む週が、その月の第1週。"
        "言い換えると「1日でも翌月が混ざれば、その週は翌月の第1週」", CW))
    story.append(Spacer(1, 5))
    story.append(Paragraph("具体例:", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(WEEK_ROWS, [62 * mm, W - 62 * mm], [S_CODE, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph("伺いたいこと:", S_LEAD))
    story.extend(num([
        "<b>サマリテーブル側も同じルールでしょうか。</b>"
        "もし「週の開始日が属する月」で数えておられる場合、上の例は<b>8月の週</b>になり、"
        "<b>当方の画面（9月第1週）と食い違います</b>",
        "違っていた場合、<b>どちらに合わせるべきかご意見をいただけますか。</b>"
        "当方はどちらでも実装できます",
    ]))
    story.append(Spacer(1, 3))
    story.append(Paragraph(
        "なお月次・年次は暦月・暦年の日単位で締める（週とは独立）認識です。", S_NOTE))

    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "W-2 robot_error_reports.error_type に入る値の一覧をご作成いただけますか", S_H3))
    story.append(Paragraph(
        "第3回でもお伺いした件です。<b>実装は止めずに進めます</b>"
        "（未知の値はすべて「その他」に寄せます）が、"
        "<b>一覧をいただければ分類の精度が上がります。</b>", S_BODY))
    story.append(Paragraph(
        "清掃詳細ログには、清掃開始の60分前・30分前のロボット状態を出す欄があります。"
        "表示したい6種類のうち、いただいた robots.status のコード表と突き合わせると"
        "<b>3つが対応先不明</b>です。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(STATE_ROWS, [46 * mm, W - 46 * mm], [S_CELL, S_CODE]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>網羅でなくて構いません。</b>実際に流れている値を、お手元で確認できる範囲で"
        "書き出していただけると助かります"
        "（例: 直近1か月の SELECT DISTINCT error_type の結果など）。"
        "<b>急ぎではありません。</b>届いた時点で分類ルールに追加します。", S_BODY))

    # ---------------------------------------------------------- 3
    story.append(CondPageBreak(55 * mm))
    story.append(Paragraph("3. 第3回からの継続（2件）", S_H2))
    story.append(Spacer(1, 3))
    story.append(table(CONT_ROWS, [14 * mm, 68 * mm, W - 82 * mm],
                       [S_CODE, S_CELL, S_CELL], head_color=SUB))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>継続（第1回から）</b>: DB更新用アカウントの発行"
        "（robot_error_reports / control_error_histories の "
        "status / comment / responded_at / resolved_at のみ UPDATE 可）。", S_BODY))

    # ---------------------------------------------------------- 末尾
    story.append(Paragraph("おわりに", S_H2))
    story.append(Paragraph(
        "<b>優先度をつけるとすれば D-1 → W-1 → 1の5件 → W-2 の順</b>です。", S_BODY))
    story.extend(bull([
        "<b>D-1</b> が無いと D-Secログの通信状態が出せません",
        "<b>W-1</b> は集計の数字が食い違う話なので、実装が進む前に揃えておきたいです",
        "<b>1の5件</b>と <b>W-2</b> は、当方の作業を止めていません",
    ]))
    story.append(Spacer(1, 6))
    story.append(Paragraph("引き続きよろしくお願いします。", S_NOTE))

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


if __name__ == "__main__":
    build()
