#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
工作台「报销」Tab 补丁（index.html + app.html 双文件）
三步铁律：① TABS 加项 ② render() switch 加 case ③ PERM_TAB_LABELS 加标签（app.html 无此项）
功能：上传（拖拽/选择/文件夹/压缩包）→ 排队状态 → 本人台账（筛选）→ 打印包/可编辑页面下载
幂等：含 EXPENSE_TAB_V1 标记则跳过。
"""
import re
import sys

MARK = 'EXPENSE_TAB_V1'

CSS = '''
/* ===== 报销 Tab (EXPENSE_TAB_V1) ===== */
.exp-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:12px}
.exp-head h2{margin:0;font-size:17px}
.exp-head .grow{flex:1}
.exp-btn{border:1px solid var(--border);background:#fff;color:var(--text);border-radius:10px;
  padding:7px 13px;font-size:12.5px;font-weight:700;cursor:pointer}
.exp-btn.pri{background:var(--pri);border-color:var(--pri);color:#fff}
.exp-btn:active{transform:scale(.97)}
.exp-drop{border:2px dashed #c7d7ee;border-radius:14px;background:#f7faff;padding:22px 16px;text-align:center;
  color:#5b6b83;font-size:13px;cursor:pointer;transition:.15s}
.exp-drop.over{background:#eaf2ff;border-color:var(--pri)}
.exp-drop b{color:var(--pri)}
.exp-drop .sub{font-size:11.5px;color:#8a94a6;margin-top:6px}
.exp-kpi{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:12px 0}
.exp-kpi .c{background:#fff;border:1px solid var(--border);border-radius:12px;padding:12px 14px}
.exp-kpi .c .v{font-size:20px;font-weight:800}
.exp-kpi .c .l{font-size:11px;color:#8a94a6;margin-top:3px}
.exp-card{background:#fff;border:1px solid var(--border);border-radius:12px;padding:13px 15px;margin-bottom:12px}
.exp-job{display:flex;align-items:center;gap:10px;font-size:13px}
.exp-dot{width:9px;height:9px;border-radius:50%;background:#94a3b8;flex-shrink:0}
.exp-dot.run{background:#f59e0b;animation:pulse 1.1s infinite}
.exp-dot.done{background:#16a34a}
.exp-dot.fail{background:#dc2626}
@keyframes pulse{0%,100%{opacity:.35}50%{opacity:1}}
.exp-chips{display:flex;gap:6px;flex-wrap:wrap;margin:10px 0}
.exp-chip{border:1px solid var(--border);background:#fff;border-radius:14px;padding:4px 11px;font-size:11.5px;
  font-weight:700;color:#5b6b83;cursor:pointer}
.exp-chip.on{background:var(--pri);border-color:var(--pri);color:#fff}
.exp-toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px}
.exp-toolbar input[type=text]{flex:1;min-width:130px;border:1px solid var(--border);border-radius:10px;padding:7px 11px;font-size:13px}
.exp-tbl{width:100%;border-collapse:collapse;font-size:12.5px;background:#fff}
.exp-tbl th,.exp-tbl td{border-bottom:1px solid #eef2f7;padding:8px 9px;text-align:left;vertical-align:top}
.exp-tbl th{background:#f7fafd;font-weight:800;font-size:11.5px;color:#64748b;white-space:nowrap}
.exp-tbl tr:hover td{background:#fafcff}
.exp-st{display:inline-block;border-radius:8px;padding:1px 7px;font-size:11px;font-weight:800;white-space:nowrap}
.st-通过{background:#e7f6ec;color:#15803d}
.st-待补{background:#fef6e0;color:#a16207}
.st-待核{background:#e8f0fe;color:#1d4ed8}
.st-异常{background:#fdeaea;color:#b91c1c}
.st-重复{background:#f3e8ff;color:#7c3aed}
.st-附件{background:#eef2f7;color:#64748b}
.exp-note{color:#8a94a6;font-size:11.5px}
.exp-packs{display:flex;flex-direction:column;gap:8px}
.exp-pack{display:flex;align-items:center;gap:10px;font-size:12.5px;border:1px solid var(--border);
  border-radius:10px;padding:9px 12px;background:#fff}
.exp-pack .n{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.exp-empty{text-align:center;color:#8a94a6;font-size:13px;padding:26px 10px}
@media (max-width:768px){
  .exp-kpi{grid-template-columns:repeat(2,1fr)}
  .exp-head h2{font-size:16px}
  .exp-tbl th:nth-child(2),.exp-tbl td:nth-child(2),
  .exp-tbl th:nth-child(5),.exp-tbl td:nth-child(5){display:none}
  .exp-tbl{font-size:12px}
}
'''

JS = r'''
/* ══════════ 报销助手 Tab (EXPENSE_TAB_V1) ══════════ */
var EXP_BASE = (typeof AUTH_APIS !== 'undefined' && AUTH_APIS[0]) ? AUTH_APIS[0] : 'https://api.biorisales.site:8443';
var _exp = { rows: [], summary: {}, packs: [], job: {}, filter: { st: '', q: '' }, timer: null };

function expReq(path, opts) {
  var base = (typeof AUTH_APIS !== 'undefined' && AUTH_APIS.length) ? AUTH_APIS[0] : EXP_BASE;
  return apiFetch(base + path, opts).then(function(r) { return r.json(); });
}

function renderExpense(main) {
  main.innerHTML =
    '<div class="exp-head"><h2>🧾 报销助手</h2>' +
      '<span class="exp-note" id="expWho"></span><span class="grow"></span>' +
      '<button class="exp-btn" onclick="expenseRefresh(1)">↻ 刷新</button>' +
      '<button class="exp-btn" onclick="expDownloadPage()">📄 可编辑页面</button>' +
      '<button class="exp-btn pri" onclick="expDownloadPack()">🖨 打印包</button>' +
    '</div>' +
    '<div class="exp-drop" id="expDrop">把发票拖到这里（或 <b>点击选择</b> / <b>选文件夹</b> / ZIP 压缩包）' +
      '<div class="sub">支持 PDF / ZIP；一次可多选。上传后自动解析、校验、生成打印包</div></div>' +
    '<input type="file" id="expFile" multiple accept=".pdf,.zip,.ofd,.png,.jpg,.jpeg" style="display:none">' +
    '<input type="file" id="expDir" webkitdirectory directory multiple style="display:none">' +
    '<div class="exp-kpi" id="expKpi"></div>' +
    '<div class="exp-card" id="expJob"></div>' +
    '<div class="exp-card" id="expPacks"></div>' +
    '<div class="exp-card"><div class="exp-toolbar">' +
      '<input type="text" id="expQ" placeholder="搜索销方 / 发票号…" oninput="expFilterQ(this.value)">' +
      '<button class="exp-btn" onclick="expenseRefresh(1)">应用</button>' +
    '</div><div class="exp-chips" id="expChips"></div><div id="expTable"></div></div>';

  var drop = document.getElementById('expDrop');
  var fi = document.getElementById('expFile');
  var di = document.getElementById('expDir');
  drop.onclick = function(ev) { if (ev.target.tagName !== 'B') fi.click(); };
  drop.querySelectorAll('b')[1] && (drop.querySelectorAll('b')[1].onclick = function(ev) { ev.stopPropagation(); di.click(); });
  drop.addEventListener('dragover', function(e) { e.preventDefault(); drop.classList.add('over'); });
  drop.addEventListener('dragleave', function() { drop.classList.remove('over'); });
  drop.addEventListener('drop', function(e) {
    e.preventDefault(); drop.classList.remove('over');
    if (e.dataTransfer.files.length) expenseUpload(e.dataTransfer.files);
  });
  fi.onchange = function() { if (this.files.length) expenseUpload(this.files); this.value = ''; };
  di.onchange = function() { if (this.files.length) expenseUpload(this.files); this.value = ''; };
  expenseRefresh(1);
}

function expenseUpload(files) {
  var fd = new FormData(), ok = 0;
  for (var i = 0; i < files.length; i++) {
    var f = files[i];
    if (!/\.(pdf|zip|ofd|png|jpe?g)$/i.test(f.name)) continue;
    fd.append('files', f, f.name); ok++;
  }
  if (!ok) { showToast('⚠️ 没有可上传的发票文件（支持 PDF / ZIP）'); return; }
  showToast('⏳ 正在上传 ' + ok + ' 个文件…');
  var base = (typeof AUTH_APIS !== 'undefined' && AUTH_APIS.length) ? AUTH_APIS[0] : EXP_BASE;
  apiFetch(base + '/api/expense/upload', { method: 'POST', body: fd })
    .then(function(r) { return r.json(); })
    .then(function(d) {
      if (!d.ok) { showToast('❌ ' + (d.error || '上传失败')); return; }
      showToast('✅ 已收到 ' + (d.saved || []).length + ' 个文件' + ((d.skipped || []).length ? '，跳过 ' + d.skipped.length + ' 个' : ''));
      _exp.job = { state: 'queued' };
      expRenderJob();
      expPoll(0);
    })
    .catch(function(e) { showToast('❌ 上传失败：' + e.message); });
}

function expPoll(n) {
  if (_exp.timer) clearTimeout(_exp.timer);
  if (n > 90) return;                       // 最多轮询 ~6 分钟
  _exp.timer = setTimeout(function() {
    expReq('/api/expense/status').then(function(d) {
      if (!d.ok) return;
      _exp.job = d.job || {}; _exp.summary = d.summary || {}; _exp.packs = d.packs || [];
      expRenderJob(); expRenderKpi(); expRenderPacks();
      var st = _exp.job.state || 'idle';
      if (st === 'queued' || st === 'running') expPoll(n + 1);
      else if (st === 'done' || st === 'failed') expenseRefresh(1);
    }).catch(function() { expPoll(n + 1); });
  }, 4000);
}

function expenseRefresh(full) {
  expReq('/api/expense/status').then(function(d) {
    if (!d.ok) { showToast('❌ ' + (d.error || '获取失败')); return; }
    if (!d.allow) {
      document.getElementById('expWho').textContent = '';
      document.getElementById('expTable').innerHTML =
        '<div class="exp-empty">⏸ 你的账号还未开通报销助手权限<br><span class="exp-note">请联系刘新元开通</span></div>';
      document.getElementById('expDrop').style.display = 'none';
      document.getElementById('expKpi').innerHTML = '';
      document.getElementById('expJob').innerHTML = '';
      document.getElementById('expPacks').innerHTML = '';
      return;
    }
    document.getElementById('expWho').textContent = d.name + ' · ' + d.uid;
    _exp.job = d.job || {}; _exp.summary = d.summary || {}; _exp.packs = d.packs || [];
    expRenderKpi(); expRenderJob(); expRenderPacks();
    if (_exp.job.state === 'queued' || _exp.job.state === 'running') expPoll(0);
  }).catch(function(e) { showToast('❌ 获取失败：' + e.message); });

  expReq('/api/expense/list').then(function(d) {
    if (!d.ok) return;
    _exp.rows = d.票据 || [];
    _exp.packs = d.packs || _exp.packs;
    expRenderChips(); expRenderTable(); expRenderPacks();
  }).catch(function() {});
}

function expRenderKpi() {
  var s = _exp.summary || {}, st = s.按状态 || {};
  var el = document.getElementById('expKpi'); if (!el) return;
  el.innerHTML =
    '<div class="c"><div class="v">' + (s.票据数 || 0) + '</div><div class="l">票据（张）</div></div>' +
    '<div class="c"><div class="v">' + (s.合计 || 0).toLocaleString() + '</div><div class="l">合计（元）</div></div>' +
    '<div class="c"><div class="v" style="color:#a16207">' + ((st.待补 || 0) + (st.待核 || 0)) + '</div><div class="l">待补 / 待核</div></div>' +
    '<div class="c"><div class="v" style="color:' + ((st.异常 || 0) ? '#b91c1c' : '#16a34a') + '">' + (st.异常 || 0) + '</div><div class="l">异常（须处理）</div></div>';
}

function expRenderJob() {
  var el = document.getElementById('expJob'); if (!el) return;
  var j = _exp.job || {}, st = j.state || 'idle';
  var cls = st === 'running' ? 'run' : (st === 'done' ? 'done' : (st === 'failed' ? 'fail' : ''));
  var txt = { idle: '空闲（投递发票后自动开始）', queued: '已排队，等待处理…', running: '正在解析票据（OCR 可能需要几分钟）…',
              done: '✅ 处理完成', failed: '❌ 处理失败' }[st] || st;
  var extra = '';
  if (st === 'done' && j.result) {
    var r = j.result, c = r.按状态 || {};
    extra = '<div class="exp-note" style="margin-top:6px">' + (j.finished_at || '') +
            ' ｜ 票据 ' + (r.票据数 || 0) + ' 张 ｜ 合计 ' + (r.合计 || 0) + ' 元 ｜ 通过 ' + (c.通过 || 0) +
            ' ｜ 待补 ' + (c.待补 || 0) + ' ｜ 待核 ' + (c.待核 || 0) + ' ｜ 异常 ' + (c.异常 || 0) + '</div>';
  }
  if (st === 'failed') extra = '<div class="exp-note" style="margin-top:6px;color:#b91c1c">' +
      ((j.result && (j.result.error || '')) || '请重试').slice(0, 200) + '</div>';
  el.innerHTML = '<div class="exp-job"><span class="exp-dot ' + cls + '"></span><b>' + txt + '</b>' +
    '<span class="grow" style="flex:1"></span>' +
    (st === 'failed' ? '<button class="exp-btn" onclick="expRequeue()">↻ 重新处理</button>' : '') +
    '</div>' + extra;
}

function expRequeue() {
  var base = (typeof AUTH_APIS !== 'undefined' && AUTH_APIS.length) ? AUTH_APIS[0] : EXP_BASE;
  apiFetch(base + '/api/expense/requeue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
    .then(function(r) { return r.json(); }).then(function() { showToast('↻ 已重新排队'); expPoll(0); });
}

function expRenderChips() {
  var st = (_exp.summary || {}).按状态 || {}, el = document.getElementById('expChips'); if (!el) return;
  var order = ['通过', '待补', '待核', '异常', '重复'];
  var html = '<button class="exp-chip' + (_exp.filter.st === '' ? ' on' : '') + '" onclick="expFilterSt(\'\')">全部 ' + (_exp.rows.length || 0) + '</button>';
  order.forEach(function(k) {
    if (!st[k]) return;
    html += '<button class="exp-chip st-' + k + (_exp.filter.st === k ? ' on' : '') + '" onclick="expFilterSt(\'' + k + '\')">' + k + ' ' + st[k] + '</button>';
  });
  el.innerHTML = html;
}

function expFilterSt(k) { _exp.filter.st = k; expRenderChips(); expRenderTable(); }
function expFilterQ(v) { _exp.filter.q = (v || '').trim().toLowerCase(); expRenderTable(); }

function expRenderTable() {
  var el = document.getElementById('expTable'); if (!el) return;
  var rows = _exp.rows || [];
  if (_exp.filter.st) rows = rows.filter(function(r) { return r.状态 === _exp.filter.st; });
  if (_exp.filter.q) rows = rows.filter(function(r) {
    return ((r.销方 || '') + (r.id || '') + (r.类别 || '')).toLowerCase().indexOf(_exp.filter.q) >= 0;
  });
  if (!rows.length) { el.innerHTML = '<div class="exp-empty">还没有票据 —— 把发票拖到上面的框里试试</div>'; return; }
  var html = '<table class="exp-tbl"><thead><tr><th>日期</th><th>销方</th><th>金额</th><th>类别</th><th>状态</th><th>说明</th></tr></thead><tbody>';
  rows.slice(0, 300).forEach(function(r) {
    var note = [];
    (r.问题 || []).slice(0, 2).forEach(function(x) { note.push(x); });
    (r.提示 || []).slice(0, 1).forEach(function(x) { note.push('⚠ ' + x); });
    html += '<tr><td>' + esc(r.开票日期 || '—') + '</td><td>' + esc((r.销方 || '—').slice(0, 22)) +
      (r.推定 ? ' <span class="exp-note" title="销方名称为推定值">⚠</span>' : '') + '</td>' +
      '<td>' + (r.金额 ? Number(r.金额).toLocaleString() : '—') + '</td>' +
      '<td>' + esc(r.类别 || '—') + '</td>' +
      '<td><span class="exp-st st-' + esc(r.状态) + '">' + esc(r.状态) + '</span></td>' +
      '<td class="exp-note">' + esc(note.join('；').slice(0, 90)) + '</td></tr>';
  });
  el.innerHTML = html + '</tbody></table>' +
    (rows.length > 300 ? '<div class="exp-note" style="padding:8px">仅显示前 300 条</div>' : '');
}

function expRenderPacks() {
  var el = document.getElementById('expPacks'); if (!el) return;
  var ps = _exp.packs || [];
  if (!ps.length) { el.innerHTML = '<div class="exp-note">还没有打印包 —— 上传发票并处理完成后自动生成</div>'; return; }
  el.innerHTML = '<div style="font-weight:800;font-size:13px;margin-bottom:8px">🖨 打印包（按制度粘贴次序排好）</div>' +
    '<div class="exp-packs">' + ps.map(function(p) {
      return '<div class="exp-pack"><span>📄</span><span class="n">' + esc(p.name) + '</span>' +
        '<span class="exp-note">' + Math.round((p.size || 0) / 1024) + ' KB · ' + esc(p.time || '') + '</span>' +
        '<button class="exp-btn" onclick="expDownloadPack(\'' + esc(p.name).replace(/'/g, '') + '\')">下载</button></div>';
    }).join('') + '</div>';
}

function expDownload(path, filename) {
  var base = (typeof AUTH_APIS !== 'undefined' && AUTH_APIS.length) ? AUTH_APIS[0] : EXP_BASE;
  showToast('⏳ 正在准备下载…');
  apiFetch(base + path).then(function(r) {
    if (!r.ok) throw new Error('HTTP ' + r.status);
    return r.blob();
  }).then(function(b) {
    var u = URL.createObjectURL(b), a = document.createElement('a');
    a.href = u; a.download = filename || 'download'; document.body.appendChild(a); a.click();
    setTimeout(function() { URL.revokeObjectURL(u); a.remove(); }, 1500);
  }).catch(function(e) { showToast('❌ 下载失败：' + e.message); });
}

function expDownloadPack(name) {
  expDownload('/api/expense/pack' + (name ? '?name=' + encodeURIComponent(name) : ''), name || '报销打印包.pdf');
}
function expDownloadPage() {
  expDownload('/api/expense/page', '报销助手.html');
}
'''


def patch(path):
    s = open(path, encoding='utf-8').read()
    if MARK in s:
        return '⏭ 已打过补丁：%s' % path

    # ① TABS 加项
    anchor = re.search(r"\{ id: 'todos'[^}]*\}", s)
    assert anchor, 'TABS(todos) 锚点未命中 ' + path
    s = s[:anchor.end()] + ", { id: 'expense', label: '报销', icon: '🧾', group: 'mine', lines: ['da','ls'] }" + s[anchor.end():]

    # ② render() switch 加 case
    sw = re.search(r"case 'todos': renderTodos\(main\); break;", s)
    assert sw, 'render switch 锚点未命中 ' + path
    s = s[:sw.end()] + "\n    case 'expense': renderExpense(main); break;" + s[sw.end():]

    # ③ PERM_TAB_LABELS（index.html 有，app.html 无）
    if 'PERM_TAB_LABELS' in s:
        pl = re.search(r"\['todos', '待办'\]", s)
        if pl:
            s = s[:pl.end()] + ", ['expense', '报销']" + s[pl.end():]

    # ④ CSS + JS
    s = s.replace('</style>', CSS + '</style>', 1)
    s = s.replace('function init()', JS + '\nfunction init()', 1) if 'function init()' in s else s.replace('init();', JS + '\ninit();', 1)

    open(path, 'w', encoding='utf-8').write(s)
    return '✅ 已打补丁：%s（%d → %d 字节）' % (path, len(open(path, encoding='utf-8').read()), len(s))


if __name__ == '__main__':
    for p in sys.argv[1:]:
        print(patch(p))
