#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""批量修「静默丢数据」形态（带安全闸）。

安全闸：只有当 gov_search 插入之前 **确有** `INSERT INTO gov_raw`、且两者之间
没有 `commit` 时才动手（= 真的会把 gov_raw 那条一起回滚的形态）。
凡是只写 gov_search、或已经 commit 过的，一律不碰。

改法：在手动 gov_search 插入语句前，按同缩进插入一行 `<连接变量>.commit()`。
逐文件 ast 校验 + 备份，幂等（已含标记则跳过）。
"""
import ast
import io
import os
import re
import shutil
import sys

D = "/root/gov_crawler"
ARCH = os.path.join(D, "Archive")
DRY = "--dry-run" in sys.argv
MARK = "先提交 gov_raw"

CONN = re.compile(r"^\s*([A-Za-z_]\w*)\s*=\s*sqlite3\.connect\(", re.M)
EXEC_START = re.compile(r"^\s*([A-Za-z_][\w\.]*)\.(execute|executemany)\(")
RAW_INS = re.compile(r"INSERT\s+(?:OR\s+(?:IGNORE|REPLACE)\s+)?INTO\s+gov_raw", re.I)
GS_INS = re.compile(r"INSERT\s+(?:OR\s+REPLACE\s+)?INTO\s+gov_search", re.I)

fixed, skipped, no_conn, no_risk = [], [], [], []

for fn in sorted(os.listdir(D)):
    if not fn.startswith("crawl_") or not fn.endswith(".py"):
        continue
    p = os.path.join(D, fn)
    src = io.open(p, encoding="utf-8", errors="ignore").read()
    if not GS_INS.search(src) or MARK in src:
        continue
    m = CONN.search(src)
    if not m:
        no_conn.append(fn)
        continue
    connvar = m.group(1)

    lines = src.split("\n")
    hit_lines = [i for i, l in enumerate(lines) if GS_INS.search(l)]
    out = list(lines)
    inserted = 0
    seen = set()
    for hl in reversed(hit_lines):
        j = hl
        while j >= 0 and not EXEC_START.match(out[j]):
            j -= 1
        if j < 0 or j in seen:
            continue
        seen.add(j)
        # ── 安全闸：往上找最近的 gov_raw 插入，检查中间有没有 commit ──
        ctx = "\n".join(out[max(0, j - 40):j])
        has_raw = bool(RAW_INS.search(ctx))
        has_commit = bool(re.search(r"\bcommit\s*\(", ctx))
        if not (has_raw and not has_commit):
            continue
        ind = re.match(r"^(\s*)", out[j]).group(1)
        out[j:j] = [
            ind + "# 2026-09-22: 先提交 gov_raw —— 库上触发器已维护 FTS，下面这条手动写入会因",
            ind + "#   rowid 重复而 IntegrityError；不先 commit 会把 gov_raw 那条一并回滚（静默丢数据）",
            ind + "%s.commit()" % connvar,
        ]
        inserted += 1

    if not inserted:
        no_risk.append(fn)
        continue
    new = "\n".join(out)
    try:
        ast.parse(new)
    except SyntaxError as e:
        skipped.append("%s(%s)" % (fn, str(e)[:36]))
        continue
    if not DRY:
        os.makedirs(ARCH, exist_ok=True)
        bak = os.path.join(ARCH, fn + ".bak_20260922_precommit")
        if not os.path.exists(bak):
            shutil.copy2(p, bak)
        io.open(p, "w", encoding="utf-8").write(new)
    fixed.append(fn)

print("=" * 96)
print("命中「真危险形态」（gov_raw 插入 → 手动 gov_search 插入、中间无 commit）")
print("=" * 96)
for fn in fixed:
    print("  ✅ " + fn)
print()
print("已改 %d 个%s" % (len(fixed), "  [DRY-RUN 未落盘]" if DRY else "  [已落盘]"))
print("安全闸拦下（形态不危险，未碰）%d 个" % len(no_risk))
if skipped:
    print("⚠️ 语法失败跳过 %d 个: %s" % (len(skipped), ", ".join(skipped[:5])))
if no_conn:
    print("⚠️ 探不到连接变量 %d 个: %s" % (len(no_conn), ", ".join(no_conn[:5])))
