#!/usr/bin/env python3
"""Generate the remaining 5 SVGs for client-update-flow per fireworks-tech-graph rules.
- as-is flow.svg (flowchart, top-down, grid x=120 y=80)
- as-is sequence.svg (lifelines, ViewBox h = 80 + N*50)
- to-be module-delta.svg / flow-delta.svg / sequence-delta.svg (green/amber/red delta)
"""
import os

# ============ as-is flow.svg ============
def gen_as_is_flow():
    W, H = 1280, 1100
    L = []
    emit = L.append
    emit('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" font-family="Segoe UI, Helvetica, Arial, sans-serif">' % (W, H))
    emit('  <defs>')
    emit('    <marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="10" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#475569"/></marker>')
    emit('    <marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="10" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#c62828"/></marker>')
    emit('    <style>')
    emit('      .title { font-size:18px; font-weight:700; fill:#7f1d1d; }')
    emit('      .cap { font-size:13px; font-weight:700; fill:#475569; }')
    emit('      .start { fill:#d4edda; stroke:#155724; stroke-width:1.6; }')
    emit('      .step  { fill:#ffffff; stroke:#3b82f6; stroke-width:1.4; }')
    emit('      .ok    { fill:#d4edda; stroke:#155724; stroke-width:1.4; }')
    emit('      .warn  { fill:#fff3cd; stroke:#856404; stroke-width:1.4; }')
    emit('      .gap   { fill:#ffe0e0; stroke:#c62828; stroke-width:1.7; stroke-dasharray:5 3; }')
    emit('      .dec   { fill:#e0e7ff; stroke:#4338ca; stroke-width:1.5; }')
    emit('      .lbl   { font-size:12px; font-weight:600; fill:#0f172a; text-anchor:middle; }')
    emit('      .sub   { font-size:10px; fill:#475569; text-anchor:middle; }')
    emit('      .elab  { font-size:10.5px; fill:#475569; }')
    emit('      .elabR { font-size:10.5px; fill:#c62828; }')
    emit('      .legend { font-size:11px; fill:#475569; }')
    emit('    </style>')
    emit('  </defs>')
    emit('  <rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, H))
    emit('  <text x="24" y="32" class="title">as-is &#183; flow &#183; harness-toolkit update 默认无参数路径</text>')

    # Grid: cols at x=120, 480, 840 (centers); rows at y=80,160,240,360,440,560,680,760,880,960
    # Snap helpers
    def cx(col): return [200, 540, 900][col]  # centers of three columns
    def lbl_xy(x, y, w, h): return (x + w//2, y + h//2)

    NW, NH = 220, 50  # standard process node
    DW, DH = 200, 56  # decision diamond (use rect rounded for simplicity)

    # Column 0 (left), 1 (middle), 2 (right)
    def P(col, row, w=NW, h=NH):
        x = cx(col) - w//2
        y = 100 + row * 90
        return x, y, w, h

    # Nodes (col, row, fill_class, label, sub=None)
    nodes = [
        (0, 0, 'start', '用户: harness-toolkit update', '(默认无参)'),
        (0, 1, 'gap',   '无版本检查 ⚠', '需求"更新提示"维度空白'),
        (0, 2, 'step',  'runUpdate(sourceDir)', '扫描 ROOT_FILES / skills / gates / teams'),
        (0, 3, 'dec',   'sha256 hash (EOL 规整)', ''),
        (1, 2, 'ok',    'skipped-up-to-date', 'hash 相同'),
        (1, 3, 'gap',   'created (dest 缺失) ⚠', '用户删除也被恢复'),
        (1, 4, 'warn',  'skipped-conflict', 'hash 不同, 保留 dest'),
        (2, 0, 'dec',   '是 CLAUDE.md?', ''),
        (2, 1, 'ok',    '完全跳过 (不对称)', 'update.ts 不管 CLAUDE.md'),
        (2, 2, 'dec',   '是 settings.*?', ''),
        (2, 3, 'ok',    '字段 union merge ✓', '不删用户项'),
        (2, 4, 'warn',  '保留 dest 原文', '进 pendingConflicts'),
    ]
    # Apply / report nodes
    apply_x, apply_y, apply_w, apply_h = cx(0)-110, 720, 220, 50
    report_x, report_y, report_w, report_h = cx(0)-110, 810, 220, 50
    # conflict resolution decision
    cf_x, cf_y, cf_w, cf_h = cx(0)-100, 600, 200, 56
    branches = [
        (1, 6, 'step', '--interactive + TTY', '逐文件 y/N 提问'),
        (1, 7, 'gap',   '--force', 'copyFileSync 整文件覆盖 ⚠⚠'),
        (2, 6, 'warn',  '默认非交互', ''),
        (2, 7, 'warn',  'exit(1), 不引导', '需求要"提示客户确认"'),
    ]

    # ===== EDGES FIRST =====
    def edge(s, e,cls='arr', color='#475569', dash=None, width=1.5, elab=None, elabR=False, elab_off=(-12,-8)):
        sx, sy = s; ex, ey = e
        d = ' stroke-dasharray="%s"' % dash if dash else ''
        c = color
        emit('  <path d="M %d %d L %d %d" stroke="%s" stroke-width="%s" fill="none" marker-end="url(#%s)"%s/>' % (sx, sy, ex, ey, c, width, cls, d))
        if elab:
            mx, my = (sx+ex)//2, (sy+ey)//2
            cls2 = 'elabR' if elabR else 'elab'
            emit('  <text x="%d" y="%d" class="%s">%s</text>' % (mx+elab_off[0], my+elab_off[1], cls2, elab))

    def R(x,y,w,h): return (x+w, y+h//2)
    def LE(x,y,w,h): return (x, y+h//2)
    def T(x,y,w,h): return (x+w//2, y)
    def B(x,y,w,h): return (x+w//2, y+h)

    # vertical chain col0: row0 -> row1 -> row2 -> row3
    p_prev = B(*P(0,0))
    n1 = P(0,1); p_next = T(*n1); edge(p_prev, p_next, color='#c62828', cls='arrR', dash='5 3', elab='无版本检查', elabR=True)
    p_prev = B(*P(0,1)); n2 = P(0,2); p_next = T(*n2); edge(p_prev, p_next)
    p_prev = B(*P(0,2)); n3 = P(0,3); p_next = T(*n3); edge(p_prev, p_next)

    # from decision (0,3) to 3 branches: (1,2)(1,3)(1,4)
    s = R(*P(0,3))
    e = LE(*P(1,2)); edge(s, e, elab='hash 相同')
    s = R(*P(0,3))
    # for (1,3) gap, route via intermediate x
    e = LE(*P(1,3))
    midx = (s[0]+e[0])//2
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="#c62828" stroke-width="1.5" fill="none" marker-end="url(#arrR)" stroke-dasharray="5 3"/>' % (s[0], s[1]+10, midx, s[1]+10, midx, e[1], e[0], e[1]))
    emit('  <text x="%d" y="%d" class="elabR">dest 缺失</text>' % (midx+6, s[1]+4))
    s = R(*P(0,3))
    e = LE(*P(1,4))
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="#475569" stroke-width="1.5" fill="none" marker-end="url(#arr)"/>' % (s[0], s[1]+20, midx, s[1]+20, midx, e[1], e[0], e[1]))
    emit('  <text x="%d" y="%d" class="elab">hash 不同</text>' % (midx+6, s[1]+26))

    # from conflict warn node (1,4) -> decision CLAUDE.md (2,0)? Jump up. Better: route conflict -> top-right decision chain.
    # Actually CLAUDE/settings decision is reached FROM the 'hash different' path. Restructure: hash different -> (2,4) "保留 dest 原文" which is the conflict; THEN at apply time we differentiate.
    # Simplify: from (1,4) "skipped-conflict" go down to apply_x via column 0.
    s = B(*P(1,4))
    e = T(apply_x, apply_y, apply_w, apply_h)
    # route: down then left to apply column
    midy = (s[1]+e[1])//2
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="#475569" stroke-width="1.4" fill="none" marker-end="url(#arr)"/>' % (s[0], s[1], s[0], midy, e[0], midy, e[0], e[1]))

    # Also: (1,2) up-to-date and (1,3) created -> apply
    s = B(*P(1,2)); e = T(apply_x, apply_y, apply_w, apply_h)
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="#475569" stroke-width="1.4" fill="none" marker-end="url(#arr)"/>' % (s[0], s[1], s[0], (s[1]+e[1])//2, e[0], (s[1]+e[1])//2, e[0], e[1]))
    s = B(*P(1,3)); e = T(apply_x, apply_y, apply_w, apply_h)
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="#c62828" stroke-width="1.4" fill="none" marker-end="url(#arrR)" stroke-dasharray="5 3"/>' % (s[0], s[1], s[0], (s[1]+e[1])//2, e[0], (s[1]+e[1])//2, e[0], e[1]))

    # apply -> report
    s = B(apply_x, apply_y, apply_w, apply_h); e = T(report_x, report_y, report_w, report_h)
    emit('  <path d="M %d %d L %d %d" stroke="#475569" stroke-width="1.5" fill="none" marker-end="url(#arr)"/>' % (s[0], s[1], e[0], e[1]))

    # conflict decision -> 3 branches
    s = B(report_x, report_y, report_w, report_h)  # use report bottom as conflict decision source? Actually conflict decision is BEFORE apply.
    # Let's add a "conflict decision" between conflict-warn and apply: cf_x/cf_y already defined.
    # Actually pendingConflicts>0 decision should be after sync loop, before apply. Re-place: it's at (0,6).
    # Insert into nodes - already past that, add it here as separate rect.
    emit('  <rect x="%d" y="%d" width="%d" height="%d" rx="6" fill="#e0e7ff" stroke="#4338ca" stroke-width="1.5"/>' % (cf_x, cf_y, cf_w, cf_h))
    emit('  <text x="%d" y="%d" class="lbl">pendingConflicts&gt;0?</text>' % (cf_x+cf_w//2, cf_y+22))
    emit('  <text x="%d" y="%d" class="sub">(在 sync 完成后)</text>' % (cf_x+cf_w//2, cf_y+38))

    # Move apply/report down; the conflict decision sits BETWEEN warn and apply.
    # Re-route warn -> conflict-decision -> branches
    # (overwrite the earlier warn->apply edge by drawing conflict decision arrow first — both will show, ok)
    s = B(*P(1,4))
    e = T(cf_x, cf_y, cf_w, cf_h)
    emit('  <path d="M %d %d L %d %d" stroke="#475569" stroke-width="1.4" fill="none" marker-end="url(#arr)"/>' % (s[0], s[1], e[0]+cf_w//2, e[1]))

    # conflict -> 3 branches: --interactive (1,6), --force (1,7), default (2,6)
    s = R(cf_x, cf_y, cf_w, cf_h)
    b1 = P(1, 6); e1 = LE(*b1)
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="#475569" stroke-width="1.4" fill="none" marker-end="url(#arr)"/>' % (s[0], s[1], (s[0]+e1[0])//2, s[1], (s[0]+e1[0])//2, e1[1], e1[0], e1[1]))
    emit('  <text x="%d" y="%d" class="elab">是 --interactive</text>' % ((s[0]+e1[0])//2-50, s[1]-6))

    b2 = P(1, 7); e2 = LE(*b2)
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="#c62828" stroke-width="1.5" fill="none" marker-end="url(#arrR)" stroke-dasharray="5 3"/>' % (s[0], s[1]+15, (s[0]+e2[0])//2, s[1]+15, (s[0]+e2[0])//2, e2[1], e2[0], e2[1]))
    emit('  <text x="%d" y="%d" class="elabR">是 --force ⚠⚠</text>' % ((s[0]+e2[0])//2-50, s[1]+30))

    b3 = P(2, 6); e3 = LE(*b3)
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="#856404" stroke-width="1.4" fill="none" marker-end="url(#arr)"/>' % (s[0], s[1]+30, (s[0]+e3[0])//2, s[1]+30, (s[0]+e3[0])//2, e3[1], e3[0], e3[1]))
    emit('  <text x="%d" y="%d" class="elab">默认非交互</text>' % ((s[0]+e3[0])//2-30, s[1]+46))

    # branches -> apply (or for --force/default exit to report directly)
    s = B(*P(1,6)); e = T(apply_x, apply_y, apply_w, apply_h)
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="#475569" stroke-width="1.4" fill="none" marker-end="url(#arr)"/>' % (s[0], s[1], s[0], (s[1]+e[1])//2, e[0], (s[1]+e[1])//2, e[0], e[1]))
    s = B(*P(1,7)); e = T(apply_x, apply_y, apply_w, apply_h)
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="#c62828" stroke-width="1.4" fill="none" marker-end="url(#arrR)" stroke-dasharray="5 3"/>' % (s[0], s[1], s[0], (s[1]+e[1])//2, e[0], (s[1]+e[1])//2, e[0], e[1]))
    s = B(*P(2,6)); e = T(report_x, report_y, report_w, report_h)
    emit('  <text x="%d" y="%d" class="elabR">(exit 1, 不进 apply)</text>' % (s[0]+10, s[1]+30))

    # ===== NODES =====
    for (col, row, cls, lbl, sub) in nodes + branches:
        x, y, w, h = P(col, row)
        emit('  <rect x="%d" y="%d" width="%d" height="%d" rx="8" fill="#ffffff" class="%s"/>' % (x, y, w, h, cls))
        cy = y + h//2
        if sub:
            emit('  <text x="%d" y="%d" class="lbl">%s</text>' % (x+w//2, cy-4, lbl))
            emit('  <text x="%d" y="%d" class="sub">%s</text>' % (x+w//2, cy+12, sub))
        else:
            emit('  <text x="%d" y="%d" class="lbl">%s</text>' % (x+w//2, cy+5, lbl))

    # apply & report nodes
    emit('  <rect x="%d" y="%d" width="%d" height="%d" rx="8" fill="#ffffff" class="step"/>' % (apply_x, apply_y, apply_w, apply_h))
    emit('  <text x="%d" y="%d" class="lbl">应用非冲突 + 字段合并</text>' % (apply_x+apply_w//2, apply_y+apply_h//2-4))
    emit('  <text x="%d" y="%d" class="sub">updateGitignore + installHooks/Permissions</text>' % (apply_x+apply_w//2, apply_y+apply_h//2+12))
    emit('  <rect x="%d" y="%d" width="%d" height="%d" rx="22" fill="#d4edda" class="start"/>' % (report_x, report_y, report_w, report_h))
    emit('  <text x="%d" y="%d" class="lbl">formatUpdateReport</text>' % (report_x+report_w//2, report_y+report_h//2+5))

    # Legend
    LG_Y = H - 60
    emit('  <rect x="40" y="%d" width="1200" height="44" fill="#f8fafc" stroke="#cbd5e1" rx="6"/>' % LG_Y)
    emit('  <text x="56" y="%d" class="legend" font-weight="700">Legend:</text>' % (LG_Y+26))
    legend_items = [
        ('ok', '#d4edda #155724', '安全/已守护'),
        ('step', '#ffffff #3b82f6', '普通流程'),
        ('warn', '#fff3cd #856404', '警告/不引导'),
        ('gap', '#ffe0e0 #c62828', '与需求冲突 (红色虚线)'),
    ]
    lx = 130
    for cls, colors, txt in legend_items:
        fill, stroke = colors.split()
        emit('  <rect x="%d" y="%d" width="18" height="14" rx="3" fill="%s" stroke="%s" stroke-width="1.4"/>' % (lx, LG_Y+14, fill, stroke))
        emit('  <text x="%d" y="%d" class="legend">%s</text>' % (lx+24, LG_Y+26, txt))
        lx += 220

    emit('</svg>')
    p = 'docs/4plus1/delta/client-update-flow/as-is/rendered/flow.svg'
    with open(p, 'w', encoding='utf-8') as f:
        f.write('\n'.join(L))
    print('flow written lines=%d size=%d' % (len(L), os.path.getsize(p)))


# ============ as-is sequence.svg ============
def gen_as_is_sequence():
    # 7 lifelines; messages vertical. viewBox height = 80 + N*50
    lanes = [
        ('用户', 80),
        ('cli.ts', 240),
        ('update.ts', 400),
        ('init.ts', 560),
        ('客户端 FS', 720),
    ]
    NL = len(lanes)
    W = 880
    messages = [
        ('harness-toolkit update', 0, 1),
        ('runUpdate(targetDir, opts)', 1, 2),
        ('scaffoldDir() / STANDALONE_SKILLS', 2, 3),
        ('sourceDir', 3, 2, True),
        ('逐文件 sha256 读 src+dest', 2, 4),
        ('hash (or null)', 4, 2, True),
        ('dest 缺失 -> created (用户删除也被恢复 ⚠)', 2, 4, False, True),
        ('hash 相同 -> skip', 2, 4, True),
        ('hash 不同 -> pendingConflicts (保留 dest)', 2, 2, False, False, True),
        ('settings.* -> installHooks/Permissions', 2, 3),
        ('写回 settings.local.json (不删项)', 3, 4),
        ('默认非交互非 force -> exit 1 (不引导 ⚠)', 2, 0, False, True),
        ('--interactive + TTY -> 逐文件 y/N', 2, 0, True),
        ('--force -> 整文件覆盖 ⚠⚠', 2, 4, False, True),
        ('UpdateResult', 2, 1, True),
        ('formatUpdateReport', 1, 0),
    ]
    H = 100 + len(messages) * 56 + 150
    L = []
    emit = L.append
    emit('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" font-family="Segoe UI, Helvetica, Arial, sans-serif">' % (W, H))
    emit('  <defs>')
    emit('    <marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="10" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#475569"/></marker>')
    emit('    <marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="10" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#c62828"/></marker>')
    emit('    <style>')
    emit('      .title { font-size:18px; font-weight:700; fill:#7f1d1d; }')
    emit('      .lane-lbl { font-size:13px; font-weight:700; text-anchor:middle; }')
    emit('      .msg { font-size:10.5px; fill:#1f2937; }')
    emit('      .msgR { font-size:10.5px; fill:#c62828; font-weight:600; }')
    emit('      .msgOK { font-size:10.5px; fill:#155724; }')
    emit('      .note { font-size:11px; fill:#4338ca; font-style:italic; }')
    emit('      .legend { font-size:11px; fill:#475569; }')
    emit('    </style>')
    emit('  </defs>')
    emit('  <rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, H))
    emit('  <text x="24" y="32" class="title">as-is &#183; sequence &#183; harness-toolkit update 默认路径</text>')

    # Lane labels and lifelines
    lane_top = 70
    lane_bot = H - 90
    for name, x in lanes:
        emit('  <rect x="%d" y="%d" width="120" height="28" rx="5" fill="#f1f5f9" stroke="#cbd5e1"/>' % (x-60, lane_top-30))
        emit('  <text x="%d" y="%d" class="lane-lbl">%s</text>' % (x, lane_top-10, name))
        emit('  <line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="4 3"/>' % (x, lane_top, x, lane_bot))

    # Messages
    y = lane_top + 30
    for msg in messages:
        if len(msg) == 3:
            text, src, dst = msg; dashed=False; red=False; ok=False; loop=False
        elif len(msg) == 4:
            text, src, dst, dashed = msg; red=False; ok=False; loop=False
        elif len(msg) == 5:
            text, src, dst, dashed, red = msg; ok=False; loop=False
        elif len(msg) == 6:
            text, src, dst, dashed, red, loop = msg; ok=False
        x1 = lanes[src][1]; x2 = lanes[dst][1]
        cls = 'arrR' if red else 'arr'
        color = '#c62828' if red else ('#155724' if ok else '#475569')
        dash_attr = ' stroke-dasharray="4 3"' if dashed else ''
        emit('  <line x1="%d" y1="%d" x2="%d" y2="%d" stroke="%s" stroke-width="1.5" marker-end="url(#%s)"%s/>' % (x1, y, x2, y, color, cls, dash_attr))
        midx = (x1+x2)//2
        cls2 = 'msgR' if red else ('msgOK' if ok else 'msg')
        emit('  <text x="%d" y="%d" class="%s" text-anchor="middle">%s</text>' % (midx, y-6, cls2, text))
        y += 56

    # Closing note
    emit('  <text x="40" y="%d" class="msgR" font-weight="700">全程无版本提示: 用户不知道该不该 update, 也不知道装了哪版</text>' % (H-50))

    emit('</svg>')
    p = 'docs/4plus1/delta/client-update-flow/as-is/rendered/sequence.svg'
    with open(p, 'w', encoding='utf-8') as f:
        f.write('\n'.join(L))
    print('sequence written lines=%d size=%d' % (len(L), os.path.getsize(p)))


# ============ Main ============
if __name__ == '__main__':
    gen_as_is_flow()
    gen_as_is_sequence()
