# -*- coding: utf-8 -*-
"""第11回 PDF「確認事項（第11回）」の自己点検。

原稿の文字がすべて PDF に出ているか、フォントが日本語フォントで埋め込まれているか等を見る。
PDF のパスは第1引数で渡せる。省略時はこのスクリプトと同じディレクトリの
DWatch_確認事項_第11回_20261002.pdf。原稿は第2引数で渡せる。省略時は
★送付版★ docs/handoff/TeamWorks確認事項_第11回_20261002_送付版.md。
終了コードは OK なら 0、NG なら 1。

★2026-10-01 の改訂 — 送付版から作る★
  ユーザー指示「PDFは依頼したい分とか確認部分だけにしてください。解決したもの
  とかは記載しないで、長いので」により、PDF は送付版から作るようになった。
  全文の原稿（…_20261002.md）は社内の記録で、PDF には出さない。点検で足したもの:
    4. ★設問が25問あること★（送付版の ### 見出しを数え、全部 PDF に出ていること）
    7. FORBIDDEN に、送付版から外した章・表の目印を追加
       （取り下げる設問 / 伺った内容 / 訂正 / 参考 — 索引 / 索引 / 数字の出所 /
         本書の構成 / お急ぎの度合い / 8章〜10章・12章への参照）。
       ★「訂正」「索引」は送付版の本文で使っていないことを確かめてから足した★

★このスクリプトは生成スクリプト（make_teamworks_qa11_pdf.py）を import しない★
見るのは「PDF から抽出したテキスト」と「原稿の .md」だけ。
原稿の読み方（記法の外し方）もこのファイルの中で独自に書いてある。
生成側の実装が変わっても、この点検の結果は変わらない。

★第9回からの変更★
  第9回は原稿の文言を must に書き写して照合していた（約1,600行）。
  第11回は生成側が原稿の .md を読むようになったので、点検側も★原稿の .md を
  読んで期待値を作る★。原稿を直しても点検スクリプトは直さなくてよい。

点検の項目
  1. ページ数とサイズ
  2. フォント埋め込み — BIZ UDGothic だけであること（中国語向けフォントが無いこと）
  3. ★ が PDF のテキストに1つも無いこと
  4. 原稿の見出しが全部 PDF に出ていること（取りこぼしの検出）
     ★設問（### の見出し）が25問あること★
  5. 原稿の表の行数と、PDF に出た表の行数が一致すること
     （表ごとに、行のセルを左から繋いだ文字列を、PDF の抽出テキストの中で
       原稿の順に探す。見つかった行数を原稿の行数と突き合わせる）
  6. 原稿の全文が PDF に出ていること（★取りこぼし禁止★）
     （原稿の各行から記法を外した文字列を、原稿の順に探す）
  7. 出てはいけない文字列（FORBIDDEN）が無いこと
     ★.env.local の DB のユーザー名・パスワードは、このスクリプトにも書かず、
       実行時に .env.local から読んで照合する。値は出力にも出さない★
  8. 豆腐（グリフ欠け）
  9. 空白ページ

★照合は空白を詰めて行う★（全角空白・NBSP・ゼロ幅を含む）。
PDF ではバッククォートを記法として外しているので、照合の前に両方から落とす。
ページを跨ぐ文はフッター（D-Watch / DFA Robotics とページ番号）で分断されるので、
各ページからフッターを落としてから繋いだテキストで照合する。

★この点検があっても、全ページの目視確認は省かないこと★（pdf/README.md）。
枠のはみ出し・改ページ位置・識別子の語中改行は文字の有無では見られない。

必要なもの: pypdf。reportlab は 8.（豆腐の検査）にだけ使い、
無ければその項目を飛ばす（他の検査は走る）。
"""
import os
import re
import sys
from urllib.parse import unquote

try:
    sys.stdout.reconfigure(errors="backslashreplace")  # cp932 で落ちないように
except Exception:
    pass

try:
    from pypdf import PdfReader
except ImportError:
    print("★NG★ pypdf が入っていない。`pip install pypdf` が必要。")
    sys.exit(1)

HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.normpath(os.path.join(HERE, "..", "..", ".."))
# ★既定は送付版★（2026-10-01 の改訂。PDF は送付版から作る）。第2引数で原稿を変えられる。
SRC = (sys.argv[2] if len(sys.argv) > 2 else
       os.path.join(REPO, "docs", "handoff", "TeamWorks確認事項_第11回_20261002_送付版.md"))
# 送付版の設問の数（全文の原稿の「質問25問」と同じ。送付版で削った設問は無い）
EXPECTED_Q = 17
ENV = os.path.join(REPO, ".env.local")
PDF = (sys.argv[1] if len(sys.argv) > 1 else
       os.path.join(HERE, "DWatch_確認事項_第11回_20261002.pdf"))
if not os.path.exists(PDF):
    print("★NG★ PDF が見つからない: %s" % PDF)
    print("先に make_teamworks_qa11_pdf.py を実行するか、"
          "PDF のパスを第1引数で渡すこと。")
    sys.exit(1)
if not os.path.exists(SRC):
    print("★NG★ 原稿が見つからない: %s" % SRC)
    sys.exit(1)

raw = open(PDF, "rb").read()
reader = PdfReader(PDF)
pages = [p.extract_text() or "" for p in reader.pages]
text = "\n".join(pages)
md_lines = open(SRC, encoding="utf-8").read().splitlines()
ng = []


def sq(s):
    """空白（全角・NBSP・ゼロ幅を含む）とバッククォートを落とす。"""
    return re.sub(r"[\s\u00a0\u3000\u200b\ufeff]+", "", s).replace("`", "")


def strip_footer(p, num):
    """ページのフッター（D-Watch / DFA Robotics とそのページの番号）を落とす。

    ページ番号はそのページの番号に限って落とす（本文の数字を食わないように）。
    """
    return re.sub(r"D-Watch\s*/\s*DFA\s*Robotics\s*%d(?!\d)" % num, "", p, count=1)


flat = sq(text)
flat_nofoot = sq("".join(strip_footer(p, i) for i, p in enumerate(pages, 1)))


def has(s):
    t = sq(s)
    return t in flat or t in flat_nofoot


def md_plain(s):
    """原稿の行内記法（★ / ** / ~~ / `）を外す。"""
    return s.replace("★", "").replace("**", "").replace("~~", "").replace("`", "")


def find_in_order(items, hay):
    """items（(行番号, 文字列) の並び）を hay の中で順に探す。

    見つかったものは位置を進める。見つからなかったものは位置を進めずに記録する
    （1件の欠落で後続が全部 NG にならないように）。
    """
    pos = 0
    missing = []
    found = 0
    for ln, s in items:
        t = sq(s)
        if not t:
            continue
        k = hay.find(t, pos)
        if k < 0:
            missing.append((ln, s))
        else:
            found += 1
            pos = k + len(t)
    return found, missing


# ---------------------------------------------------------------- 原稿を読む
RE_SEP = re.compile(r"^\|(\s*:?-{3,}:?\s*\|)+\s*$")
headings = []      # (行番号, 見出しの文字列)
tables = []        # [(行番号, [セル…]), …] の並び
body_lines = []    # (行番号, 記法を外した文字列) — 全文照合用（表の行も含む）
in_code = False
cur_table = None
for i, line in enumerate(md_lines, 1):
    if line.startswith("```"):
        in_code = not in_code
        cur_table = None
        continue
    if in_code:
        body_lines.append((i, line))
        continue
    if line.startswith("|"):
        if cur_table is None:
            cur_table = []
            tables.append(cur_table)
        if RE_SEP.match(line):
            continue
        cells = [md_plain(c.strip()) for c in line.strip()[1:-1].split("|")]
        cur_table.append((i, cells))
        body_lines.append((i, "".join(cells)))
        continue
    cur_table = None
    s = line.strip()
    if not s or re.match(r"^-{3,}$", s):
        continue
    m = re.match(r"^(#{1,6})\s+(.*)$", s)
    if m:
        headings.append((i, md_plain(m.group(2))))
        body_lines.append((i, md_plain(m.group(2))))
        continue
    s = re.sub(r"^>\s?", "", s)
    s = re.sub(r"^- ", "", s)          # PDF では「・」に置き換わる
    body_lines.append((i, md_plain(s)))

print("原稿:", SRC)
print("  %d 行 / 見出し %d / 表 %d（見出し行を含め %d 行） / 照合する行 %d"
      % (len(md_lines), len(headings), len(tables),
         sum(len(t) for t in tables), len(body_lines)))

# ---------------------------------------------------------------- 1.
print("\n== 1. ページ数とサイズ ==")
print("ページ数:", len(reader.pages), " サイズ:", len(raw), "bytes",
      "(%.1f KB)" % (len(raw) / 1024))
print("抽出文字数:", len(text), " 空白を詰めた文字数:", len(flat))
if len(flat) < 2000:
    print("  ★NG★ 抽出できた文字が少なすぎる。テキストが図形化されている疑い")
    ng.append("抽出文字数が少なすぎる")

# ---------------------------------------------------------------- 2.
print("\n== 2. フォント埋め込み（★最重要★） ==")
print("/FontFile2 出現回数:", raw.count(b"/FontFile2"))
# 埋め込まれているフォントが BIZ UDGothic だけであること。
# サブセット接頭辞（AAAAAA+）が付いているものが埋め込みフォント。
# 接頭辞の無いものは reportlab が必ず1つ書き出す標準14フォント（Helvetica）で、
# フォントファイルを持たず本文にも使われていない（第7回〜第9回の PDF にも同じ1件がある）。
allfonts = sorted(set(re.findall(rb"/BaseFont\s*/([A-Za-z0-9+\-,._]+)", raw)))
embedded = [f.decode() for f in allfonts if re.match(rb"^[A-Z]{6}\+", f)]
standard = [f.decode() for f in allfonts if not re.match(rb"^[A-Z]{6}\+", f)]
print("埋め込みフォント:", embedded)
print("非埋め込みの標準フォント（reportlab の既定・未使用）:", standard)
if not [f for f in embedded if "BIZ" in f]:
    print("  ★NG★ BIZ UDGothic が埋め込まれていない")
    ng.append("BIZ UDGothic が埋め込まれていない")
notbiz = [f for f in embedded if "BIZ" not in f]
if notbiz:
    print("  ★NG★ BIZ UDGothic 以外のフォントが埋め込まれている:", notbiz)
    ng.append("BIZ UDGothic 以外の埋め込みフォント %s" % notbiz)
if len(embedded) != raw.count(b"/FontFile2"):
    print("  ★注意★ 埋め込み名の数と /FontFile2 の数が合わない")
print("うち Bold:", [f for f in embedded if "Bold" in f] or "なし",
      " Regular:", [f for f in embedded if "Bold" not in f] or "なし")
# 中国語向けフォントが混ざっていないこと（CLAUDE.md §0-3）
CN = [b"SimSun", b"NSimSun", b"SimHei", b"MicrosoftYaHei", b"MSYaHei",
      b"MSGothic", b"NotoSansSC", b"NotoSerifSC", b"NotoSansCJKsc",
      b"SourceHanSansSC", b"SourceHanSerifSC", b"FangSong", b"KaiTi",
      b"STSong", b"STHeiti", b"STKaiti", b"STFangsong", b"PingFang",
      b"DengXian", b"MingLiU", b"PMingLiU", b"DFKai", b"YouYuan", b"LiSu"]
cn_hit = [n.decode() for n in CN if n in raw]
print("中国語向けフォント:", cn_hit if cn_hit else "混入なし")
if cn_hit:
    print("  ★NG★ 中国語向けフォントが混ざっている")
    ng.append("中国語向けフォント %s" % cn_hit)

# ---------------------------------------------------------------- 3.
print("\n== 3. ★ が PDF に出ていないこと ==")
md_star = sum(l.count("★") for l in md_lines)
pdf_star = text.count("★")
print("原稿の ★: %d 個 / PDF の ★: %d 個" % (md_star, pdf_star))
if pdf_star:
    print("  ★NG★ PDF に ★ が残っている")
    ng.append("PDF に ★ が %d 個" % pdf_star)
# 記法の残りかすも見ておく（** / ~~ / バッククォート）
leftover = {k: text.count(k) for k in ("**", "~~", "`")}
print("記法の残り（** / ~~ / `）:", leftover)
if any(leftover.values()):
    print("  ★NG★ 記法が外れずに残っている")
    ng.append("記法の残り %s" % leftover)

# ---------------------------------------------------------------- 4.
print("\n== 4. 原稿の見出しが全部 PDF に出ていること ==")
h_found, h_missing = find_in_order(headings, flat_nofoot)
print("見出し: 原稿 %d / PDF で見つかった %d" % (len(headings), h_found))
for ln, s in h_missing:
    if has(s):
        print("  ★注意★ %d行の見出しは PDF にあるが、順序が原稿と違う: %s" % (ln, s))
    else:
        print("  ★NG★ %d行の見出しが PDF に無い: %s" % (ln, s))
if h_missing:
    ng.append("見出しの欠落・順序違い %d 件" % len(h_missing))
# ★設問の数★ — 送付版では ### を設問にだけ使う約束（make_teamworks_qa11_pdf.py の冒頭）
questions = [(ln, md_plain(l[4:].strip())) for ln, l in enumerate(md_lines, 1)
             if l.startswith("### ")]
q_found, q_missing = find_in_order(questions, flat_nofoot)
print("設問（### の見出し）: 原稿 %d 問 / PDF で見つかった %d 問 / 期待 %d 問"
      % (len(questions), q_found, EXPECTED_Q))
for ln, s in questions:
    print("  %4d行 %s" % (ln, s[:60]))
if len(questions) != EXPECTED_Q or q_found != EXPECTED_Q:
    print("  ★NG★ 設問の数が %d 問ではない" % EXPECTED_Q)
    ng.append("設問の数 原稿 %d / PDF %d（期待 %d）"
              % (len(questions), q_found, EXPECTED_Q))

# ---------------------------------------------------------------- 5.
print("\n== 5. 表の行数（原稿 / PDF） ==")
# 表の見出し行は改ページ時に PDF で繰り返されるが、順に探すので余分は読み飛ばす。
pos = 0
t_bad = 0
total_md = total_pdf = 0
for ti, tbl in enumerate(tables, 1):
    found = 0
    miss = []
    for ln, cells in tbl:
        t = sq("".join(cells))
        k = flat_nofoot.find(t, pos)
        if k < 0:
            miss.append(ln)
        else:
            found += 1
            pos = k + len(t)
    total_md += len(tbl)
    total_pdf += found
    mark = "OK" if found == len(tbl) else "★NG★"
    print("  表%-3d %4d行目  原稿 %2d 行 / PDF %2d 行  %s%s"
          % (ti, tbl[0][0], len(tbl), found, mark,
             ("  欠落: %s行目" % miss) if miss else ""))
    if miss:
        t_bad += 1
print("合計: 原稿 %d 行 / PDF %d 行（表 %d 個中、不一致 %d 個）"
      % (total_md, total_pdf, len(tables), t_bad))
if t_bad:
    ng.append("表の行数の不一致 %d 個" % t_bad)

# ---------------------------------------------------------------- 6.
print("\n== 6. 原稿の全文が PDF に出ていること（取りこぼし禁止） ==")
b_found, b_missing = find_in_order(body_lines, flat_nofoot)
print("照合した行: %d / 見つかった %d / 欠落 %d"
      % (len([1 for _, s in body_lines if sq(s)]), b_found, len(b_missing)))
for ln, s in b_missing[:30]:
    where = "順序違い" if has(s) else "欠落"
    print("  ★NG★ %d行（%s）: %s" % (ln, where, s[:80]))
if b_missing:
    ng.append("本文の欠落・順序違い %d 行" % len(b_missing))
# 文字数でも突き合わせる（記法を外した原稿の文字数 ≒ PDF の文字数）
md_chars = len(sq("".join(s for _, s in body_lines)))
print("記法を外した原稿の文字数 %d / PDF（フッター除く）の文字数 %d"
      % (md_chars, len(flat_nofoot)))
print("  差 %d は、箇条書きの「・」・改ページで繰り返した表の見出し行の分"
      % (len(flat_nofoot) - md_chars))

# ---------------------------------------------------------------- 7.
print("\n== 7. 出てはいけない文字列（FORBIDDEN） ==")
FORBIDDEN = [
    # ---- 社内の呼び名・社内資料への参照（TeamWorks は知らない） ----
    "指示書",
    "docs/",
    "lib/",
    "decisions.md",
    "CLAUDE.md",
    "本番DBそのもの",
    "IDENTIFIED BY",
]
# 第11回の送付版は「お願い」「訂正」「SHOW GRANTS」を本文で正当に使う（P-14 のお願い、P-1 の訂正後の式、
# P-13 の権限確認）ので、第10回の禁止語からは外した（2026-10-05）。D-番号は下で正規表現で見る。
ALLOWED_CONTEXT = {}
f_hit = []
import re as _re
if _re.search(r"D-\d{3}", flat_nofoot):
    print("  ★NG★ D-番号（社内の決定番号）が出ている")
    f_hit.append("D-番号")
for w in FORBIDDEN:
    t = sq(w)
    n_all = flat_nofoot.count(t)
    n_ok = sum(flat_nofoot.count(sq(c)) for c in ALLOWED_CONTEXT.get(w, []))
    n_bad = n_all - n_ok
    if ALLOWED_CONTEXT.get(w):
        n_md = sum(sq(md_plain("\n".join(md_lines))).count(sq(c))
                   for c in ALLOWED_CONTEXT[w])
        print("  %-14s 出現 %d（うち許可した形 %s: %d / 原稿 %d） → 禁止の形 %d"
              % (w, n_all, "・".join(ALLOWED_CONTEXT[w]), n_ok, n_md, n_bad))
        if n_ok != n_md:
            print("  ★NG★ 許可した形の数が原稿と合わない")
            f_hit.append(w + "（許可した形の数）")
    else:
        print("  %-14s 出現 %d" % (w, n_all))
    if n_bad:
        f_hit.append(w)
# .env.local の DB 認証情報（値はこのスクリプトに書かない・出力にも出さない）
secrets = []
if os.path.exists(ENV):
    env = {}
    for l in open(ENV, encoding="utf-8"):
        m = re.match(r"^\s*([A-Z0-9_]+)\s*=\s*(.*?)\s*$", l)
        if m:
            env[m.group(1)] = m.group(2).strip().strip('"').strip("'")
    for k in ("DATABASE_USER", "DATABASE_PASSWORD"):
        if env.get(k):
            secrets.append((k, env[k]))
    mu = re.match(r"^[a-z]+://([^:@/]*):([^@/]*)@", env.get("DATABASE_URL", ""))
    if mu:
        secrets.append(("DATABASE_URL のユーザー名", unquote(mu.group(1))))
        secrets.append(("DATABASE_URL のパスワード", unquote(mu.group(2))))
    secrets = [(k, v) for k, v in secrets if v]
    s_hit = []
    for k, v in secrets:
        if (v in text or sq(v) in flat or sq(v) in flat_nofoot
                or v.encode("utf-8") in raw):
            s_hit.append(k)
    print("  .env.local の DB 認証情報: %d 個の値を照合（値は表示しない） → %s"
          % (len(secrets), ("★検出★ " + ", ".join(s_hit)) if s_hit else "検出なし"))
    short = [k for k, v in secrets if len(v) < 4]
    if short:
        print("  ★注意★ 4文字未満の値があり、偶然の一致で誤検出しうる:", short)
    if s_hit:
        f_hit.append(".env.local の認証情報（%s）" % ", ".join(s_hit))
    if not secrets:
        print("  ★NG★ .env.local から DB の認証情報を読めなかった（照合できていない）")
        ng.append(".env.local の認証情報を読めない")
else:
    print("  ★NG★ .env.local が無い（認証情報の照合ができない）:", ENV)
    ng.append(".env.local が無い")
print("FORBIDDEN の検出:", f_hit if f_hit else "なし")
if f_hit:
    ng.append("FORBIDDEN %s" % f_hit)

# ---------------------------------------------------------------- 8.
print("\n== 8. 豆腐（グリフ欠け） ==")
# 抽出テキストの全文字を BIZ UDGothic の cmap に引き当てる。
# 字が無い文字は PDF 上で □ になる。㎡ / Σ / ÷ / ⇔ / ①〜⑨ / 〜 / ＝ 等の確認。
try:
    from reportlab.pdfbase import pdfmetrics
    from reportlab.pdfbase.ttfonts import TTFont
    pdfmetrics.registerFont(TTFont("JP", "C:/Windows/Fonts/BIZ-UDGothicR.ttc",
                                   subfontIndex=0))
    pdfmetrics.registerFont(TTFont("JP-B", "C:/Windows/Fonts/BIZ-UDGothicB.ttc",
                                   subfontIndex=0))
    c2g_r = pdfmetrics.getFont("JP").face.charToGlyph
    c2g_b = pdfmetrics.getFont("JP-B").face.charToGlyph
    chars = sorted(set(text) - set("\n\r\t "))
    missing_r = [c for c in chars if ord(c) not in c2g_r]
    missing_b = [c for c in chars if ord(c) not in c2g_b]
    print("本文に出る文字種:", len(chars))
    print("BIZ UDGothic R に字が無い文字:", missing_r if missing_r else "なし")
    print("BIZ UDGothic B に字が無い文字:", missing_b if missing_b else "なし")
    if missing_r or missing_b:
        ng.append("グリフ欠け R=%s B=%s" % (missing_r, missing_b))
except Exception as e:
    print("  ★この検査は飛ばした★（reportlab かフォントが無い）:", e)
tofu = sorted(set(c for c in text if c in "\ufffd□"))
print("置換文字・豆腐そのものの混入:", tofu if tofu else "なし")
if tofu:
    ng.append("豆腐 %s" % tofu)
# 原稿にある文字が、PDF の文字種から抜けていないか（記法の文字を除く）
md_chars_set = set(md_plain("".join(md_lines))) - set(" \t#>|-")
lost = sorted(md_chars_set - set(text) - set("\u3000"))
print("原稿にあって PDF に1度も出ない文字:", lost if lost else "なし")
if lost:
    ng.append("PDF に出ない文字 %s" % lost)

# ---------------------------------------------------------------- 9.
print("\n== 9. 空白ページ ==")
blank = []
for i, t in enumerate(pages, 1):
    body = sq(strip_footer(t, i))
    print("  p%-3d 抽出 %5d 文字 / 本文 %5d 文字" % (i, len(t), len(body)))
    if len(body) < 20:
        blank.append(i)
print("空白ページ:", blank if blank else "なし")
if blank:
    ng.append("空白ページ %s" % blank)

# ---------------------------------------------------------------- 結果
print("\n== 結果 ==")
print("ページ数: %d / サイズ: %.1f KB" % (len(reader.pages), len(raw) / 1024))
if ng:
    print("★NG★")
    for x in ng:
        print("  -", x)
else:
    print("OK（全項目）")
sys.exit(0 if not ng else 1)
