#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""P0 前端补丁：待办模块 对内/对外 分层（工具/index.html + 工具/app.html 同步）。

两个入口的待办代码字节相同，故同一组替换同时应用。
每处替换都断言命中次数，任何一处不中即整体回滚不落盘。
"""
import io
import os
import re
import shutil
import sys
import time

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
FILES = [os.path.join(ROOT, '工具', 'index.html'), os.path.join(ROOT, '工具', 'app.html')]

# ── 1) 类型表 + 分层辅助函数 ──────────────────────────────────
NEW_TABLES = r'''var CATS = {
  internal_project: '🎯 立项跟进', internal_complaint: '🔴 客诉跟进',
  internal_test_plan: '🧪 测试方案', internal_other: '⚪ 其他（客户需求）',
  external_visit: '🔵 拜访计划', external_test_req: '🧪 测试需求',
  external_test_result: '✅ 测试结果', external_project: '🟢 项目进度',
  external_appointment: '📅 邀约及计划',
  prep: '🔵 访前准备', post: '🟢 访后跟进', complaint: '🔴 客诉',
  custom: '🟣 定制需求', meeting: '🟡 会议邀约', travel: '✈️ 出差计划', other: '⚪ 其他'
};
var CAT_COLORS = {
  internal_project: '#8b5cf6', internal_complaint: '#d93025', internal_test_plan: '#0891b2', internal_other: '#9aa0a6',
  external_visit: '#3b82f6', external_test_req: '#0d904f', external_test_result: '#0d904f', external_project: '#0d904f', external_appointment: '#e37400',
  prep: '#3b82f6', post: '#0d904f', complaint: '#d93025', custom: '#8b5cf6', meeting: '#e37400', travel: '#0891b2', other: '#9aa0a6'
};
/* P0：对内 4 类 / 对外 5 类。scope 由 type 派生；旧 7 类保留兼容显示与兜底映射 */
var TYPE_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'
};
var LEGACY2TYPE = {
  prep: 'external_visit', post: 'external_project', complaint: 'internal_complaint',
  custom: 'internal_other', meeting: 'external_appointment', travel: 'external_appointment', other: 'internal_other'
};
var NEW_TYPES = ['external_visit', 'external_test_req', 'external_test_result', 'external_project', 'external_appointment',
  'internal_project', 'internal_complaint', 'internal_test_plan', 'internal_other'];
function todoType(t) { var c = t.type || t.cat || 'other'; return TYPE_SCOPE[c] ? c : (LEGACY2TYPE[c] || 'internal_other'); }
function todoScope(t) { return TYPE_SCOPE[todoType(t)] || 'internal'; }
function normalizeTodo(t) {
  var tp = todoType(t); t.type = tp; t.cat = tp; t.scope = TYPE_SCOPE[tp];
  if (typeof t.owner !== 'string') t.owner = '';
  return t;
}
function typeOptions(sel) {
  function opt(k) { return '<option value="' + k + '"' + (k === sel ? ' selected' : '') + '>' + CATS[k] + '</option>'; }
  return '<optgroup label="对外（客户侧）">' + NEW_TYPES.slice(0, 5).map(opt).join('') + '</optgroup>' +
         '<optgroup label="对内（内部协同）">' + NEW_TYPES.slice(5).map(opt).join('') + '</optgroup>';
}'''

# ── 2) renderTodos：分组渲染 + 范围筛选 + 表单补 owner/技术协同/截止必填 ──
NEW_RENDER = r'''  var nExt = filtered.filter(function(t) { return todoScope(t) === 'external'; });
  var nInt = filtered.filter(function(t) { return todoScope(t) === 'internal'; });
  function sect(title, items, col) {
    if (!items.length) return '';
    return '<div style="margin:14px 0 8px;font-weight:800;font-size:13px;color:' + col + ';border-bottom:1px solid #e5e7eb;padding-bottom:4px">' +
      title + ' <span style="font-weight:600;color:#9aa0a6">' + items.length + ' 条</span></div>' +
      items.map(function(t) { return renderTodoRow(t); }).join('');
  }
  main.innerHTML = '<div class="card"><div class="card-header" style="flex-wrap:wrap;gap:10px"><h3>✅ 待办事项 <span class="tag tag-blue">' + active + ' 待处理</span></h3><div class="todo-filters"><button class="btn btn-sm ' + (todoFilter === 'open' ? 'active' : '') + '" onclick="setTodoFilter(\'open\')">未关闭</button><button class="btn btn-sm ' + (todoFilter === 'closed' ? 'active' : '') + '" onclick="setTodoFilter(\'closed\')">已关闭</button><button class="btn btn-sm ' + (todoFilter === 'all' ? 'active' : '') + '" onclick="setTodoFilter(\'all\')">全部</button><span style="width:12px"></span><button class="btn btn-sm ' + (todoScopeFilter === 'all' ? 'active' : '') + '" onclick="setTodoScope(\'all\')">全部范围</button><button class="btn btn-sm ' + (todoScopeFilter === 'external' ? 'active' : '') + '" onclick="setTodoScope(\'external\')">对外</button><button class="btn btn-sm ' + (todoScopeFilter === 'internal' ? 'active' : '') + '" onclick="setTodoScope(\'internal\')">对内</button><span style="width:12px"></span><button class="btn btn-sm" onclick="exportTodos()">📤 导出</button></div></div>' +
    '<div class="card-body"><div class="todo-form"><input class="input" id="todoText" placeholder="输入待办内容…" onkeydown="if(event.key===\'Enter\')addTodo()"><select id="todoCat" class="input">' + typeOptions('') + '</select><input class="input" id="todoOwner" placeholder="负责人" style="max-width:92px"><select id="todoClient" class="input"><option value="">关联客户</option>' + custOpts + '</select><input type="date" id="todoDue" class="input" title="截止时间（必填）"><label class="text-muted" style="font-size:12px;display:flex;align-items:center;gap:4px;white-space:nowrap"><input type="checkbox" id="todoTech">需技术协同</label><button class="btn btn-primary" onclick="addTodo()">+ 新增</button></div>' +
    '<div class="todo-list">' + ((nExt.length + nInt.length) ? (sect('📤 对外 · 客户侧', nExt, '#1F4E9C') + sect('🏠 对内 · 内部协同', nInt, '#5f6368')) : '<div class="todo-empty"><span class="icon">✅</span>暂无待办<br><span style="font-size:12px">输入内容并点击新增吧</span></div>') + '</div></div></div>' +
    ((state.user.role === 'admin' || state.user.role === 'gm' || state.user.role === 'manager') ? renderClosedFeed() : '');'''

# ── 3) renderTodoRow ──
NEWROW = r'''function renderTodoRow(t) {
  normalizeTodo(t);
  var cat = t.type;
  var color = t.done ? '#e5e7eb' : CAT_COLORS[cat];
  var dd = t.due || '';
  var dueTag = '';
  if (dd && !t.done) {
    var todayStr = today();
    if (dd < todayStr) dueTag = '<span class="tag tag-red">⚠ 过期 ' + dd + '</span>';
    else if (dd === todayStr) dueTag = '<span class="tag tag-orange">今天</span>';
    else dueTag = '<span class="tag tag-gray">📅 ' + dd.slice(5) + '</span>';
  } else if (!dd && !t.done) dueTag = '<span class="tag tag-gray">无截止</span>';
  return '<div class="todo-item' + (t.done ? ' done' : '') + '" style="border-left-color:' + color + '"><input type="checkbox"' + (t.done ? ' checked' : '') + ' onchange="toggleTodo(\'' + t.id + '\')"><div class="todo-text"><div>' + esc(t.text) + '</div><div class="todo-meta"><span style="color:' + CAT_COLORS[cat] + ';font-weight:700">' + CATS[cat] + '</span>' + (t.owner ? '<span class="tag tag-blue">👤 ' + esc(String(t.owner).slice(0, 8)) + '</span>' : '') + (t.tech_support ? '<span class="tag tag-orange">🛠 需技术协同</span>' : '') + (t.client ? '<span>@' + esc(t.client.slice(0, 16)) + '</span>' : '') + dueTag + (t.note ? '<span class="text-muted">📝 ' + esc(t.note.slice(0, 30)) + '</span>' : '') + (t.conclusion ? '<span class="text-ok">💬 ' + esc(t.conclusion.slice(0, 40)) + '</span>' : '') + '</div></div><div class="todo-actions"><select class="input" style="width:auto;font-size:11px;padding:2px 4px" onchange="changeTodoType(\'' + t.id + '\',this.value)" title="改类型">' + typeOptions(cat) + '</select><button class="btn btn-sm btn-danger" onclick="deleteTodo(\'' + t.id + '\')">删除</button></div></div>';
}'''

# ── 4) addTodo ──
NEWADD = r'''function addTodo() {
  var text = document.getElementById('todoText').value.trim();
  if (!text) return alert('请输入待办内容');
  var tp = document.getElementById('todoCat').value;
  if (!TYPE_SCOPE[tp]) return alert('请选择待办类型');
  var due = document.getElementById('todoDue').value;
  if (!due) return alert('请选择截止时间（必填）');
  var own = (document.getElementById('todoOwner').value || '').trim() || (((state.user || {}).name) || '');
  if (!own) return alert('请填写负责人');
  var tech = document.getElementById('todoTech');
  var list = loadTodos();
  list.unshift({
    id: Date.now().toString(36) + Math.random().toString(36).slice(2, 5),
    text: text, done: false, type: tp, cat: tp, scope: TYPE_SCOPE[tp], owner: own,
    tech_support: !!(tech && tech.checked),
    client: document.getElementById('todoClient').value,
    due: due, note: '', created: today()
  });
  saveTodos(list);
  pushTodos();
  document.getElementById('todoText').value = '';
  render();
}'''


def apply(fpath, pairs, dry):
    s = io.open(fpath, encoding='utf-8').read()
    orig = s
    log = []
    for label, pat, new, cnt in pairs:
        if new is None:                     # 仅计数
            n = len(re.findall(pat, s))
            log.append((label, n, cnt))
            continue
        s2, n = re.subn(pat, lambda m: new, s, flags=re.S)
        log.append((label, n, cnt))
        if n != cnt:
            print('  ❌ %s 命中 %d 次（应为 %d）→ 放弃该文件' % (label, n, cnt))
            return None, log
        s = s2
    if not dry:
        io.open(fpath, 'w', encoding='utf-8').write(s)
    return s, log


def main():
    dry = '--apply' not in sys.argv
    ts = time.strftime('%Y%m%d-%H%M%S')
    print('P0 前端补丁 —— %s' % ('DRY-RUN' if dry else 'APPLY'))
    bak = os.path.join(ROOT, '_bak_todo_p0_front_%s' % ts)
    if not dry:
        os.makedirs(bak, exist_ok=True)

    pairs = [
        ('A 类型表+分层辅助',
         r'var CATS = \{[\s\S]{0,400}?\};\s*\n\s*var CAT_COLORS = \{[^}]*\};', NEW_TABLES, 1),
        ('B+C renderTodos 整体',
         r"main\.innerHTML = '<div class=\"card\"><div class=\"card-header\" style=\"flex-wrap:wrap;gap:10px\"><h3>✅ 待办事项[\s\S]*?renderClosedFeed\(\) : ''\);",
         NEW_RENDER, 1),
        ('D renderTodoRow',
         r'function renderTodoRow\(t\) \{[\s\S]*?\n\}\n\nfunction setTodoFilter', NEWROW + '\n\nfunction setTodoFilter', 1),
        ('E addTodo',
         r'function addTodo\(\) \{[\s\S]*?\n\}\n\nfunction toggleTodo', NEWADD + '\n\nfunction toggleTodo', 1),
        ('F toggleTodo 归一化',
         r'function toggleTodo\(id\) \{\n  var list = loadTodos\(\);\n  var t = list\.find\(function\(x\) \{ return x\.id === id; \}\);\n  if \(!t\) return;\n',
         'function toggleTodo(id) {\n  var list = loadTodos();\n  var t = list.find(function(x) { return x.id === id; });\n  if (!t) return;\n  normalizeTodo(t);\n', 1),
        ('G deleteTodo 补同步',
         r"function deleteTodo\(id\) \{\n  if \(!confirm\('确定删除该待办？'\)\) return;\n  var list = loadTodos\(\)\.filter\(function\(t\) \{ return t\.id !== id; \}\);\n  saveTodos\(list\);\n  render\(\);\n\}",
         "function deleteTodo(id) {\n  if (!confirm('确定删除该待办？')) return;\n  var list = loadTodos().filter(function(t) { return t.id !== id; });\n  saveTodos(list);\n  pushTodos();\n  render();\n}", 1),
        ('H 范围筛选+改类型',
         r'(function setTodoFilter\(f\) \{ todoFilter = f; render\(\); \})',
         r'''function setTodoFilter(f) { todoFilter = f; render(); }
var todoScopeFilter = 'all';
function setTodoScope(s) { todoScopeFilter = s; render(); }
function changeTodoType(id, val) {
  var list = loadTodos();
  var t = list.find(function(x) { return x.id === id; });
  if (!t || !TYPE_SCOPE[val]) return;
  t.type = val; t.cat = val; t.scope = TYPE_SCOPE[val];
  saveTodos(list); pushTodos(); render();
}''', 1),
    ]

    ok = True
    for f in FILES:
        print('\n=== %s ===' % os.path.basename(f))
        if not dry:
            shutil.copy2(f, os.path.join(bak, os.path.basename(f)))   # 先备份原文件
        s, log = apply(f, pairs, dry)
        for label, n, cnt in log:
            print('  %s %-26s 命中 %d/%d' % ('✅' if n == cnt else '❌', label, n, cnt))
        if s is None:
            ok = False
            print('  → 该文件未改动')
        else:
            normalize = s.count('normalizeTodo(')
            print('  normalizeTodo 出现 %d 处' % normalize)
            if not dry:
                print('  已写（原文件备份在 %s）' % bak)
    print('\n%s' % ('全部命中 ✅' if ok else '有未命中 ❌'))


if __name__ == '__main__':
    main()
