#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""anchor_batch.py — WDY 记录链上锚定批处理（把整条流水线串成一条命令）

一条命令做完：锚定至至信链 → 回写 manifest 并重签 → 发布验证页同目录的
manifest.json → 幂等刷新验证页上的「链上锚定」区块 → 部署 → 出报告。

依赖同目录的 zhixin_anchor.py（transport）与 wdy_ed25519.py（签名）。

用法
----
  # 1) 先看要做哪些（不消耗额度）
  python anchor_batch.py --list

  # 2) 演练（mock 数据，不消耗额度，只改本地文件）
  python anchor_batch.py --records WDY-2026-0013-HMGZKH --mode mock

  # 3) 真锚定（消耗额度，1 元/条 或走免费额度）
  python anchor_batch.py --records WDY-2026-0013-HMGZKH

  # 4) 全部未锚定的（谨慎：会逐条消耗额度）
  python anchor_batch.py --all-unanchored

  # 5) 只刷新页面锚定块（不锚定、不改 manifest）
  python anchor_batch.py --refresh-pages

其它开关
--------
  --no-deploy   不执行部署（只改本地文件）
  --dry-run     只打印将锚定哪些、跳过哪些
  --config      zhixin.json 路径（默认取 wdy-manifest/zhixin/zhixin.json）

报告：控制台表格 + wdy-manifest/zhixin/anchor-batch-report.json（追加式留档）
"""

import argparse
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import zhixin_anchor as zx  # noqa: E402
import wdy_anchor_common as wc  # noqa: E402

ROOT = HERE.parents[3]                       # ...\cloudflare上wdy站点
WORK = ROOT / "wdy-manifest" / "work"
VERIFY = ROOT / "wdy.org" / "certified" / "verify"
CONFIG_DEFAULT = ROOT / "wdy-manifest" / "zhixin" / "zhixin.json"
REPORT = ROOT / "wdy-manifest" / "zhixin" / "anchor-batch-report.json"
ED_KEY = WORK / "wdy-2026-rot1.key"
ORIGINAL_DIRS = [WORK, WORK / "ndvhc-snapshot", WORK / "crett-snapshot", WORK / "crett-reports"]

MONO = "word-break:break-all;font:500 13px/1.7 ui-monospace,Menlo,Consolas,monospace"


# ───────────────────────── 记录发现 ─────────────────────────
def work_manifest(rid):
    p = WORK / f"{rid}.manifest.json"
    return p if p.exists() else None


def find_original(manifest):
    fn = (manifest.get("file") or {}).get("filename")
    if not fn:
        return None
    for d in ORIGINAL_DIRS:
        p = d / fn
        if p.exists():
            return p
    return None


def list_records():
    out = []
    for p in sorted(WORK.glob("WDY-2026-*.manifest.json")):
        try:
            m = json.loads(p.read_text(encoding="utf-8"))
        except Exception:
            continue
        rid = m.get("record_id", p.stem)
        out.append({
            "record_id": rid,
            "manifest": p,
            "anchored": bool((m.get("anchors") or {}).get("zhixin")),
            "page": (VERIFY / rid / "index.html").exists() if rid else False,
            "original": find_original(m),
            "title": m.get("title", ""),
        })
    return out


# ───────────────────────── 页面锚定区块渲染 / 幂等刷新 ─────────────────────────
def render_anchor_block(rid, a):
    """由 manifest 的 anchors.zhixin 渲染四语「链上锚定」区块。"""
    ev = a.get("ev_id", "")
    tx = a.get("tx_id", "")
    bh = a.get("block_height")
    tt = a.get("tx_time", "")
    sm3 = a.get("hash_sm3", "")
    cert = a.get("cert_url", "")
    scan = f"https://zxscan.qq.com/cert/ev?evId={ev}"
    return f'''  <!-- ANCHOR:BEGIN -->
  <h2 style="font-size:19px;margin:32px 0 4px"
    ><span data-zh>链上锚定（至信链）</span><span data-en>On-chain anchor (ZhiXinChain)</span
    ><span data-es>Anclaje en cadena (ZhiXinChain)</span><span data-pt>Ancoragem em cadeia (ZhiXinChain)</span></h2>
  <p style="color:var(--muted);font-size:13.5px"
    ><span data-zh>除 WDY 存证外，本记录的电子证据哈希另已锚定至至信链——腾讯联合生态伙伴基于国产开源「长安链」搭建的联盟链，由链方签发存证证书，第三方可在链方证书页独立核验。</span
    ><span data-en>In addition to the WDY record, this record's evidence hash is anchored to ZhiXinChain — a consortium chain built on ChainMaker by Tencent and ecosystem partners — which issues its own certificate, independently verifiable at the chain operator's certificate page.</span
    ><span data-es>Además del registro WDY, el hash de la prueba de este registro está anclado en ZhiXinChain — cadena de consorcio construida sobre ChainMaker por Tencent y socios del ecosistema — que emite su propio certificado, verificable de forma independiente.</span
    ><span data-pt>Além do registro WDY, o hash da evidência deste registro está ancorado na ZhiXinChain — cadeia de consórcio construída sobre a ChainMaker pela Tencent e parceiros do ecossistema — que emite seu próprio certificado, verificável de forma independente.</span></p>
  <dl>
    <div><dt><span data-zh>存证 ID</span><span data-en>Evidence ID</span><span data-es>ID de prueba</span><span data-pt>ID da evidência</span></dt>
      <dd style="font:500 13px/1.7 ui-monospace,Menlo,Consolas,monospace"><code>{ev}</code></dd></div>
    <div><dt><span data-zh>上链交易</span><span data-en>Chain transaction</span><span data-es>Transacción en cadena</span><span data-pt>Transação na cadeia</span></dt>
      <dd style="{MONO}"><code>{tx}</code></dd></div>
    <div><dt><span data-zh>区块高度</span><span data-en>Block height</span><span data-es>Altura de bloque</span><span data-pt>Altura do bloco</span></dt>
      <dd>{bh}</dd></div>
    <div><dt><span data-zh>上链时间</span><span data-en>Anchored at</span><span data-es>Anclado el</span><span data-pt>Ancorado em</span></dt>
      <dd>{tt}（<span data-zh>链方时间源：北斗卫星授权时间</span><span data-en>chain time source: BeiDou-authorized</span><span data-es>fuente de tiempo: hora autorizada por BeiDou</span><span data-pt>fonte de tempo: hora autorizada pelo BeiDou</span>）</dd></div>
    <div><dt><span data-zh>电子证据哈希（SM3）</span><span data-en>Evidence hash (SM3)</span><span data-es>Hash de prueba (SM3)</span><span data-pt>Hash da evidência (SM3)</span></dt>
      <dd style="{MONO}"><code>{sm3}</code></dd></div>
    <div><dt><span data-zh>链上存证主体</span><span data-en>On-chain account holder</span><span data-es>Titular en cadena</span><span data-pt>Titular na cadeia</span></dt>
      <dd><span data-zh>天津渤海智能机器人有限责任公司（统一社会信用代码 91120222MAC95RTA43）——WDY 的链上锚定由该主体运营，主体信息以链方证书公示为准。</span
        ><span data-en>Tianjin Bohai Intelligent Robot Co., Ltd. (unified social credit code 91120222MAC95RTA43) — WDY's on-chain anchoring is operated under this entity; the entity shown on the chain operator's certificate governs.</span
        ><span data-es>Tianjin Bohai Intelligent Robot Co., Ltd. (código de crédito social unificado 91120222MAC95RTA43) — el anclaje en cadena de WDY opera bajo esta entidad.</span
        ><span data-pt>Tianjin Bohai Intelligent Robot Co., Ltd. (código de crédito social unificado 91120222MAC95RTA43) — a ancoragem em cadeia da WDY opera sob esta entidade.</span></dd></div>
  </dl>
  <p style="margin-top:14px;font-size:13.5px"
    ><span data-zh><strong>核验：</strong>打开链方签发的存证证书（PDF，含印章与二维码）：<a href="{cert}" style="color:var(--gold)">下载证书 →</a>　在线证书页：<a href="{scan}" style="color:var(--gold)">zxscan.qq.com →</a></span
    ><span data-en><strong>Verify:</strong> open the chain operator's certificate (PDF, with seal and QR): <a href="{cert}" style="color:var(--gold)">download certificate →</a>　Online certificate page: <a href="{scan}" style="color:var(--gold)">zxscan.qq.com →</a></span
    ><span data-es><strong>Verificación:</strong> abra el certificado del operador de la cadena (PDF con sello y QR): <a href="{cert}" style="color:var(--gold)">descargar certificado →</a></span
    ><span data-pt><strong>Verificação:</strong> abra o certificado do operador da cadeia (PDF com selo e QR): <a href="{cert}" style="color:var(--gold)">baixar certificado →</a></span></p>
  <!-- ANCHOR:END -->
'''


def upsert_anchor_block(page_path, block):
    """幂等写入：有标记则替换标记之间内容，无标记则插到 <div class="terms"> 之前。"""
    s = page_path.read_text(encoding="utf-8")
    if "<!-- ANCHOR:BEGIN -->" in s and "<!-- ANCHOR:END -->" in s:
        s2 = re.sub(r"  <!-- ANCHOR:BEGIN -->.*?  <!-- ANCHOR:END -->\n", block, s, flags=re.S)
        mode = "replaced"
    else:
        if '<div class="terms">' not in s:
            raise RuntimeError("页面里找不到 <div class=\"terms\"> 插入点")
        s2 = s.replace('  <div class="terms">', block + '\n  <div class="terms">', 1)
        mode = "inserted"
    if s2 != s:
        page_path.write_text(s2, encoding="utf-8")
    return mode


def refresh_intl_blocks(rid, m, page):
    """按 manifest.anchors 刷新 TSA / OpenTimestamps 区块（幂等；无则跳过）。"""
    acts = []
    anchors = m.get("anchors") or {}
    if anchors.get("tsa"):
        txt = page.read_text(encoding="utf-8")
        after = "  <!-- ANCHOR:END -->" if "  <!-- ANCHOR:END -->" in txt else None
        mode = wc.upsert_block(page, "  <!-- ANCHOR:TSA:BEGIN -->", "  <!-- ANCHOR:TSA:END -->",
                               wc.render_tsa_block(rid, anchors["tsa"]), insert_after=after)
        acts.append("page-tsa-" + mode)
    if anchors.get("opentimestamps"):
        txt = page.read_text(encoding="utf-8")
        if "  <!-- ANCHOR:TSA:END -->" in txt:
            after = "  <!-- ANCHOR:TSA:END -->"
        elif "  <!-- ANCHOR:END -->" in txt:
            after = "  <!-- ANCHOR:END -->"
        else:
            after = None
        mode = wc.upsert_block(page, "  <!-- ANCHOR:OTS:BEGIN -->", "  <!-- ANCHOR:OTS:END -->",
                               wc.render_ots_block(rid, anchors["opentimestamps"]), insert_after=after)
        acts.append("page-ots-" + mode)
    return acts


# ───────────────────────── 单条处理 ─────────────────────────
def process(rec, cfg, mode=None, refresh_only=False, dry=False):
    rid = rec["record_id"]
    res = {"record_id": rid, "actions": [], "ok": True}
    mpath, page = rec["manifest"], VERIFY / rid / "index.html"
    m = json.loads(mpath.read_text(encoding="utf-8"))
    anch = (m.get("anchors") or {}).get("zhixin")

    if refresh_only:
        anchors_all = m.get("anchors") or {}
        if not anchors_all:
            res["actions"].append("skip: 无锚定内容，无可刷新")
            return res
        if not page.exists():
            res["actions"].append("skip: 无验证页")
            return res
        if anch:
            res["actions"].append("page-" + upsert_anchor_block(page, render_anchor_block(rid, anch)))
        res["actions"] += refresh_intl_blocks(rid, m, page)
        return res

    if anch and not dry:
        res["actions"].append("skip: 已锚定（加 --force 可重锚）")
    else:
        if dry:
            res["actions"].append(f"would-anchor: {rec['original']}")
            return res
        if not rec["original"]:
            res["ok"] = False
            res["actions"].append("FAIL: 找不到原件（无法按文件算 SM3）")
            return res
        r = zx.anchor(m["file"]["sha256"], rid, file_path=str(rec["original"]),
                      manifest_path=str(mpath), cfg=cfg, dry_run=False, get_cert=True)
        if not r.get("ok"):
            res["ok"] = False
            res["actions"].append(f"FAIL: {r.get('error')}")
            return res
        res["anchors"] = {k: r.get(k) for k in ("evId", "txId", "blockHeight", "txTime", "chain_confirmed")}
        res["actions"].append("anchored: evId=" + str(r.get("evId")))
        zx.merge_into_manifest(str(mpath), r, str(ED_KEY), key_id="2026-rot1")
        res["actions"].append("manifest 回写并重签")
        m = json.loads(mpath.read_text(encoding="utf-8"))
        anch = (m.get("anchors") or {}).get("zhixin")

    # 发布 manifest.json（验证页同目录）
    if page.exists() and (VERIFY / rid).is_dir():
        (VERIFY / rid / "manifest.json").write_text(
            json.dumps(m, ensure_ascii=False, indent=2), encoding="utf-8")
        res["actions"].append("已发布 verify/<rid>/manifest.json")
        if anch:
            res["actions"].append("page-" + upsert_anchor_block(page, render_anchor_block(rid, anch)))
        res["actions"] += refresh_intl_blocks(rid, m, page)
    else:
        res["actions"].append("提示: 无验证页目录，跳过 manifest 发布与页面刷新（需先克隆模板出页）")
    return res


def deploy():
    p = subprocess.run([sys.executable, str(ROOT / "deploy_and_verify.py")],
                       cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8", errors="replace")
    tail = [l for l in (p.stdout or "").splitlines() if "VERIFY" in l or "checked" in l]
    return p.returncode, tail


def main():
    ap = argparse.ArgumentParser(description="WDY 链上锚定批处理")
    ap.add_argument("--records", default=None, help="逗号分隔的记录号")
    ap.add_argument("--all-unanchored", action="store_true")
    ap.add_argument("--refresh-pages", action="store_true", help="只按 manifest 刷新页面锚定块")
    ap.add_argument("--list", action="store_true", help="列出记录及状态")
    ap.add_argument("--config", default=str(CONFIG_DEFAULT))
    ap.add_argument("--mode", choices=["live", "mock"], default=None)
    ap.add_argument("--dry-run", action="store_true")
    ap.add_argument("--no-deploy", action="store_true")
    ap.add_argument("--force", action="store_true", help="已锚定的也重锚")
    a = ap.parse_args()

    recs = list_records()
    if a.list or not (a.records or a.all_unanchored or a.refresh_pages):
        print(f"{'记录号':<26}{'已锚定':<8}{'有验证页':<9}{'原件':<7}标题")
        for r in recs:
            print(f"{r['record_id']:<26}{'是' if r['anchored'] else '否':<8}"
                  f"{'是' if r['page'] else '否':<9}{'有' if r['original'] else '缺':<7}{r['title'][:34]}")
        print(f"\n合计 {len(recs)} 条，已锚定 {sum(1 for r in recs if r['anchored'])} 条，"
              f"缺原件 {sum(1 for r in recs if not r['original'])} 条")
        return

    if a.records:
        want = [x.strip() for x in a.records.split(",") if x.strip()]
        recs = [r for r in recs if r["record_id"] in want or r["record_id"].split("-")[-1] in want]
    elif a.all_unanchored:
        recs = [r for r in recs if not r["anchored"] and r["original"] and r["page"]]
    if not recs:
        print("没有匹配的记录"); return

    cfg = zx.load_config(explicit=a.config)
    if a.mode:
        cfg["mode"] = a.mode
    print(f"模式={cfg['mode']}｜待处理 {len(recs)} 条：{[r['record_id'] for r in recs]}\n")

    results = []
    for r in recs:
        res = process(r, cfg, refresh_only=a.refresh_pages, dry=a.dry_run)
        results.append(res)
        flag = "OK " if res["ok"] else "ERR"
        print(f"[{flag}] {res['record_id']}")
        for x in res["actions"]:
            print("      -", x)
        if a.force and res["ok"] and not a.dry_run and "skip: 已锚定" in res["actions"]:
            pass

    if not a.no_deploy and not a.dry_run:
        print("\n=== 部署 ===")
        rc, tail = deploy()
        for l in tail:
            print("   ", l.strip())
        print("部署返回码:", rc)

    if not a.dry_run:
        hist = json.loads(REPORT.read_text(encoding="utf-8")) if REPORT.exists() else []
        hist.append({"at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
                     "mode": cfg["mode"], "records": results})
        REPORT.write_text(json.dumps(hist, ensure_ascii=False, indent=2), encoding="utf-8")
        print("报告已追加:", REPORT)


if __name__ == "__main__":
    main()
