#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""P0 迁移：待办旧 7 类 cat → 新 9 类 type + scope + owner（幂等）。

覆盖三处：
  ① 数据/todos.json        （文件镜像 / Tunnel 兜底）
  ② CloudBase todos 集合    （前端匿名 SDK 直写的**权威**存储）
  ③ 前端由 normalizeTodo() 兜底（本脚本不碰，见 index.html/app.html）

用法：
  python3 脚本/migrate_todos_p0.py            # 干跑（默认，不写任何东西）
  python3 脚本/migrate_todos_p0.py --apply    # 真写（先自动备份）
"""
import io
import json
import os
import re
import shutil
import subprocess
import sys
import time

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
LOCAL = os.path.join(ROOT, '数据', 'todos.json')
CLI = '/Users/liuxinyuan/.npm-global/lib/node_modules/@cloudbase/cli/dist/standalone/cli.js'
ENV_ID = 'bier-sales-d0gatbvlx288724e9'
COLL = 'todos'

# ── 旧 7 类 → 新 9 类 ─────────────────────────────────────────
LEGACY2NEW = {
    'prep':      'external_visit',        # 访前准备   → 对外·拜访计划
    'post':      'external_project',      # 访后跟进   → 对外·项目进度
    'complaint': 'internal_complaint',    # 客诉       → 对内·客诉跟进
    'custom':    'internal_other',        # 定制需求   → 对内·其他（客户需求）
    'meeting':   'external_appointment',  # 会议邀约   → 对外·邀约及计划
    'travel':    'external_appointment',  # 出差计划   → 对外·邀约及计划
    'other':     'internal_other',        # 其他       → 对内·其他（客户需求）
}
SCOPE = {
    'internal_project': 'internal', 'internal_complaint': 'internal',
    'internal_test_plan': 'internal', 'internal_other': 'internal',
    'external_visit': 'external', 'external_test_req': 'external',
    'external_test_result': 'external', 'external_project': 'external',
    'external_appointment': 'external',
}
NEW_TYPES = list(SCOPE.keys())


def tmap(cat):
    """旧值 → 新值；已是新值则原样返回。"""
    if cat in SCOPE:
        return cat
    return LEGACY2NEW.get(cat, 'internal_other')


def migrate_item(t, owner):
    """幂等地补 type / scope / owner，并保持 cat 与新 type 同值（兼容旧代码路径）。"""
    old = t.get('cat') or t.get('type') or 'other'
    new = tmap(old)
    out = dict(t)
    out['type'] = new
    out['cat'] = new                       # cat 作为同值别名，旧渲染/总监视图无需改
    out['scope'] = SCOPE[new]
    out['owner'] = (out.get('owner') or owner or '').strip()
    return out, old, new


def norm(t):
    return re.sub(r'\s+', ' ', (t or '').strip())


def cli(cmds):
    """跑 tcb db nosql execute，剥掉 '-  xxx' 进度行再解析 JSON。"""
    p = subprocess.run(['node', CLI, 'db', 'nosql', 'execute',
                        '--command', json.dumps(cmds, ensure_ascii=False),
                        '--json', '-e', ENV_ID],
                       capture_output=True, text=True, timeout=180)
    raw = '\n'.join(l for l in (p.stdout or '').splitlines()
                    if not l.lstrip().startswith('- ')).strip()
    i = raw.find('{')
    if i < 0:
        raise RuntimeError('CLI 无 JSON 输出: %s / %s' % (p.stdout[:300], p.stderr[:300]))
    return json.loads(raw[i:])


def cb_find():
    r = cli([{'TableName': COLL, 'CommandType': 'QUERY',
              'Command': json.dumps({'find': COLL, 'filter': {}, 'limit': 100})}])
    return r.get('data', {}).get('results', [[]])[0]


def cb_set(user, todos):
    cmd = json.dumps({'update': COLL,
                      'updates': [{'q': {'user': user},
                                   'u': {'$set': {'todos': todos,
                                                  'updated': time.strftime('%Y-%m-%dT%H:%M:%S.000Z')}}}]})
    return cli([{'TableName': COLL, 'CommandType': 'UPDATE', 'Command': cmd}])


def main():
    apply = '--apply' in sys.argv
    print('=' * 78)
    print('P0 待办迁移 —— %s' % ('真写 APPLY' if apply else '干跑 DRY-RUN（不写任何东西）'))
    print('=' * 78)

    # ── 本地文件 ──
    store = json.load(io.open(LOCAL, encoding='utf-8'))
    users = store.get('users', {})
    print('\n【① 数据/todos.json】用户 %d 个' % len(users))
    local_plan = {}
    ldiff = {}
    for u, lst in users.items():
        if not isinstance(lst, list):
            continue
        newl = []
        for t in lst:
            m, old, new = migrate_item(t, u)
            ldiff[old + ' → ' + new] = ldiff.get(old + ' → ' + new, 0) + 1
            newl.append(m)
        local_plan[u] = newl
    closed = store.get('closed') or []
    closed_plan = []
    for t in closed:
        m, old, new = migrate_item(t, t.get('salesperson'))
        ldiff[old + ' → ' + new] = ldiff.get(old + ' → ' + new, 0) + 1
        closed_plan.append(m)
    print('   待办 %d 条 / 归档 %d 条' % (sum(len(v) for v in local_plan.values()), len(closed)))

    # ── CloudBase ──
    print('\n【② CloudBase todos 集合】')
    docs = cb_find()
    print('   文档 %d 个' % len(docs))
    cb_plan = []
    cdiff = {}
    for d in docs:
        u = d.get('user')
        lst = d.get('todos') or []
        newl = []
        for t in lst:
            m, old, new = migrate_item(t, u)
            cdiff[old + ' → ' + new] = cdiff.get(old + ' → ' + new, 0) + 1
            newl.append(m)
        cb_plan.append((u, newl, len(lst)))
        if lst:
            print('   %-12s %d 条' % (u, len(lst)))

    # ── 变更明细 ──
    print('\n【映射明细 · 本地】')
    for k in sorted(ldiff):
        print('   %-34s %d 条' % (k, ldiff[k]))
    print('【映射明细 · 云端】')
    for k in sorted(cdiff):
        print('   %-34s %d 条' % (k, cdiff[k]))

    print('\n【逐条预览 · 本地（前 12 条）】')
    shown = 0
    for u, lst in local_plan.items():
        for m in lst:
            if shown >= 12:
                break
            print('   [%s] %-30s %-20s → %s' % (u, norm(m.get('text'))[:30],
                                                m.get('type'), m.get('scope')))
            shown += 1

    if not apply:
        print('\n干跑结束，未写入。确认无误后跑：python3 脚本/migrate_todos_p0.py --apply')
        return

    # ── 备份 ──
    ts = time.strftime('%Y%m%d-%H%M%S')
    bak = os.path.join(ROOT, '_bak_todo_p0_%s' % ts)
    os.makedirs(bak, exist_ok=True)
    shutil.copy2(LOCAL, os.path.join(bak, 'todos.json'))
    io.open(os.path.join(bak, '_cloudbase_before.json'), 'w', encoding='utf-8').write(
        json.dumps(docs, ensure_ascii=False, indent=1))
    print('\n已备份 → %s' % bak)

    # ── 写本地 ──
    store['users'] = local_plan
    if closed_plan:
        store['closed'] = closed_plan
    store['updated_at'] = time.strftime('%Y-%m-%d %H:%M:%S')
    io.open(LOCAL, 'w', encoding='utf-8').write(
        json.dumps(store, ensure_ascii=False, indent=1))
    print('① 本地已写')

    # ── 写云端 ──
    for u, newl, n0 in cb_plan:
        cb_set(u, newl)
        print('② 云端 %-12s %d 条 已写' % (u, len(newl)))

    # ── 回读校验 ──
    time.sleep(2)
    print('\n=== 回读校验 ===')
    l2 = json.load(io.open(LOCAL, encoding='utf-8'))
    n = sum(len(v) for v in l2['users'].values() if isinstance(v, list))
    n0 = sum(len(v) for v in users.values() if isinstance(v, list))
    print('%s 本地条数 %d → %d' % ('✅' if n == n0 else '❌', n0, n))
    bad_l = [t.get('id') for v in l2['users'].values() if isinstance(v, list) for t in v
             if not t.get('scope') or not t.get('type') or 'owner' not in t]
    print('%s 本地缺字段 %d 条' % ('✅' if not bad_l else '❌', len(bad_l)))

    d2 = cb_find()
    n2 = sum(len(d.get('todos') or []) for d in d2)
    n1 = sum(len(d.get('todos') or []) for d in docs)
    print('%s 云端条数 %d → %d' % ('✅' if n2 == n1 else '❌', n1, n2))
    bad_c = [(d.get('user'), t.get('id')) for d in d2 for t in (d.get('todos') or [])
             if not t.get('scope') or not t.get('type') or 'owner' not in t]
    print('%s 云端缺字段 %d 条 %s' % ('✅' if not bad_c else '❌', len(bad_c), bad_c[:5]))
    print('\n迁移完成：%s' % ('全部通过 ✅' if n == n0 and n2 == n1 and not bad_l and not bad_c
                             else '存在不一致 ❌'))


if __name__ == '__main__':
    main()
