#!/usr/bin/env python3
"""P13 交付层回滚：把 p13-archived-rows.json 的 9 节点 + 19 边重新注回画布。

与删除同走 save-v2 正道（DB 直读当前真值 + 并入归档行 → 全量回写），
零服务重启，秒级生效并广播刷新所有已打开的画布页。

用法: python3 /home/kai/shared/2026-08-28/p13-archive/rollback_p13.py
"""
import sqlite3, json, time, urllib.request, urllib.error, sys

DB = "/data/workspace/kais-aigc-platform/data/db2.sqlite"
P, E = 1785508691757, 1
ARCH = "/home/kai/shared/2026-08-28/p13-archive/p13-archived-rows.json"

arch = json.load(open(ARCH))
arch_nodes = {n["id"]: n for n in arch["nodes"]}
arch_links = {l["id"]: l for l in arch["links"]}
print(f"archive: {len(arch_nodes)} nodes, {len(arch_links)} links")

con = sqlite3.connect(DB)
con.row_factory = sqlite3.Row
def rows(sql, params=()):
    return [dict(r) for r in con.execute(sql, params)]

nodes = rows("SELECT * FROM canvas_nodes WHERE project_id=? AND episodes_id=?", [P, E])
links = rows("SELECT * FROM canvas_links WHERE project_id=? AND episodes_id=?", [P, E])
branches = rows("SELECT * FROM canvas_branches WHERE project_id=? AND episodes_id=?", [P, E])
groups = rows("SELECT * FROM canvas_variant_groups WHERE project_id=? AND episodes_id=?", [P, E])

NOW = int(time.time() * 1000)

def mknode(r):
    d = json.loads(r["data"]) if r["data"] else {}
    n = {"id": r["id"], "type": r["type"], "branchId": r["branch_id"],
         "phaseIndex": r["phase_index"], "phaseName": r["phase_name"],
         "position": {"x": r["position_x"], "y": r["position_y"]},
         "size": {"width": r["size_width"], "height": r["size_height"]},
         "data": d, "state": r["state"]}
    if r["review_status"]: n["reviewStatus"] = r["review_status"]
    if r["ai_score"]: n["aiScore"] = json.loads(r["ai_score"])
    if r["is_winner"]: n["isWinner"] = True
    if r["variant_of"]: n["variantOf"] = r["variant_of"]
    if r["variant_group_id"]: n["variantGroupId"] = r["variant_group_id"]
    return n

def mklink(r):
    l = {"id": r["id"], "source": r["source_id"], "target": r["target_id"],
         "branchId": r["branch_id"]}
    if r["data_type"]: l["dataType"] = r["data_type"]
    if r["link_type"]: l["linkType"] = r["link_type"]
    if r["ref_type"]: l["refType"] = r["ref_type"]
    if r["is_explore"]: l["isExplore"] = True
    if r["is_inactive"]: l["isInactive"] = True
    return l

def archlink_to_wire(l):
    out = {"id": l["id"], "source": l["source_id"], "target": l["target_id"],
           "branchId": l["branch_id"]}
    if l.get("data_type"): out["dataType"] = l["data_type"]
    if l.get("link_type"): out["linkType"] = l["link_type"]
    if l.get("ref_type"): out["refType"] = l["ref_type"]
    if l.get("is_explore"): out["isExplore"] = True
    if l.get("is_inactive"): out["isInactive"] = True
    return out

def archnode_to_wire(n):
    d = json.loads(n["data"]) if n["data"] else {}
    w = {"id": n["id"], "type": n["type"], "branchId": n["branch_id"],
         "phaseIndex": n["phase_index"], "phaseName": n["phase_name"],
         "position": {"x": n["position_x"], "y": n["position_y"]},
         "size": {"width": n["size_width"], "height": n["size_height"]},
         "data": d, "state": n["state"]}
    if n.get("review_status"): w["reviewStatus"] = n["review_status"]
    if n.get("ai_score"): w["aiScore"] = json.loads(n["ai_score"])
    if n.get("is_winner"): w["isWinner"] = True
    if n.get("variant_of"): w["variantOf"] = n["variant_of"]
    if n.get("variant_group_id"): w["variantGroupId"] = n["variant_group_id"]
    return w

present = {n["id"] for n in nodes} | set(arch_nodes)
wire_nodes = ([mknode(n) for n in nodes]
              + [archnode_to_wire(n) for nid, n in arch_nodes.items() if nid not in {m["id"] for m in nodes}])
wire_links = [mklink(l) for l in links] + [archlink_to_wire(l) for lid, l in arch_links.items() if lid not in {m["id"] for m in links}]

dangling = [l["id"] for l in wire_links
            if l["source"] not in present or l["target"] not in present]
assert not dangling, f"dangling links: {dangling[:5]}"

graph = {
    "meta": {"projectId": P, "episodesId": E, "version": "2",
             "createdAt": NOW, "updatedAt": NOW},
    "nodes": wire_nodes,
    "links": wire_links,
    "branches": [{"id": b["id"], "label": b["label"], "status": b["status"],
                  "createdAt": b["created_at"] or NOW, "updatedAt": b["updated_at"] or NOW}
                 for b in branches],
    "variantGroups": [
        {"id": g["id"], "phaseIndex": g["phase_index"], "branchId": g["branch_id"],
         "variantNodeIds": json.loads(g["variant_node_ids"]) if g["variant_node_ids"] else [],
         **({"winnerNodeId": g["winner_node_id"]} if g["winner_node_id"] else {}),
         "selectMode": g["select_mode"]}
        for g in groups
    ],
}

body = json.dumps({"projectId": P, "episodesId": E, "graph": graph,
                   "savedBy": "hermes-p13-rollback"}).encode()
print(f"payload: {len(body)/1024:.0f} KB, nodes={len(wire_nodes)}, links={len(wire_links)}")

req = urllib.request.Request(
    "http://127.0.0.1:10588/api/canvas/v2/save-v2", data=body,
    headers={"Content-Type": "application/json"}, method="POST")
try:
    with urllib.request.urlopen(req, timeout=60) as resp:
        print("save-v2 HTTP", resp.status)
except urllib.error.HTTPError as e:
    print("save-v2 HTTP", e.code, e.read()[:800])
    sys.exit(1)
