#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
生成「客户特殊要求 · 候选复核」页面（自包含 HTML，可编辑保存）
================================================================
输入：数据/sr_review.json（由 merge_special_requirements.py --review 生成）
输出：工具/特殊要求复核.html

页面能力（按宝锐规范）：
  · 全局搜索 + 类别/置信度/校验状态筛选 + 点列头排序（可筛选·可排序·可索引）
  · 每行「采纳 / 驳回」+ localStorage 自动保存 + 导出 JSON（供合并入库）/ 导出 CSV
  · 📋 原始数据弹窗（看底层 JSON）
"""
import json
import os
import re
import subprocess
import sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SRC = os.path.join(ROOT, '数据', 'sr_review.json')
OUT = os.path.join(ROOT, '工具', '特殊要求复核.html')

TPL = """<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>客户特殊要求 · 候选复核</title>
<style>
*{box-sizing:border-box}
body{margin:0;font:14px/1.6 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif;background:#f4f6fb;color:#1a2233}
.top{background:#fff;border-bottom:2px solid #1852B6;padding:10px 18px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:20;flex-wrap:wrap}
.logo{font-weight:800;color:#1852B6;font-size:16px;letter-spacing:.5px}
.logo small{color:#7a8699;font-weight:500;margin-left:6px;font-size:12px}
.grow{flex:1}
.btn{border:1px solid #cfdcee;background:#fff;color:#1852B6;border-radius:8px;padding:6px 12px;font-size:13px;cursor:pointer}
.btn:hover{background:#f0f6ff}
.btn.pri{background:#1852B6;color:#fff;border-color:#1852B6}
.wrap{max-width:1560px;margin:16px auto;padding:0 16px}
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin-bottom:14px}
.kpi{background:#fff;border-radius:10px;padding:12px 14px;border-left:4px solid #1852B6;box-shadow:0 1px 3px rgba(16,42,90,.07)}
.kpi .v{font-size:22px;font-weight:700;color:#0f2a52}
.kpi .l{font-size:12px;color:#7a8699;margin-top:2px}
.card{background:#fff;border-radius:12px;box-shadow:0 1px 3px rgba(16,42,90,.08);padding:14px 16px}
.bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px}
.bar input[type=text],.bar select{padding:7px 10px;border:1px solid #cfdcee;border-radius:8px;font-size:13px;background:#fff}
.bar input[type=text]{flex:1;min-width:200px}
table{width:100%;border-collapse:collapse;font-size:13px}
th,td{padding:8px 9px;border-bottom:1px solid #eef2f8;vertical-align:top;text-align:left}
th{background:#f7faff;color:#41506b;font-weight:600;white-space:nowrap;cursor:pointer;position:sticky;top:56px}
th.no{cursor:default}
tbody tr:hover{background:#fbfdff}
td.c{white-space:nowrap}
.tag{display:inline-block;padding:1px 8px;border-radius:10px;font-size:12px;border:1px solid}
.t-高{background:#e8f8ee;color:#12723c;border-color:#bfe6cd}
.t-中{background:#fff7e6;color:#9a6200;border-color:#f2dca8}
.t-低{background:#fdeaea;color:#a32020;border-color:#f3c9c9}
.cat{background:#eef4ff;color:#1852B6;border-color:#cfe0ff}
.bad{background:#fdeaea;color:#a32020;border-color:#f3c9c9}
.ok{background:#e8f8ee;color:#12723c;border-color:#bfe6cd}
.pick{border:1px solid #cfdcee;background:#fff;border-radius:6px;padding:2px 8px;cursor:pointer;font-size:12px;margin-right:4px}
.pick.on.y{background:#12723c;color:#fff;border-color:#12723c}
.pick.on.n{background:#a32020;color:#fff;border-color:#a32020}
tr.y{background:#f6fdf8}
tr.n{opacity:.5}
.ev{color:#6b7688;font-size:12px}
.cont{min-width:280px}
.hide{display:none}
.modal{position:fixed;inset:0;background:rgba(10,20,40,.55);display:none;align-items:center;justify-content:center;z-index:50;padding:24px}
.modal.on{display:flex}
.mbox{background:#fff;border-radius:12px;max-width:1000px;width:100%;max-height:84vh;display:flex;flex-direction:column}
.mh{padding:12px 16px;border-bottom:1px solid #eef2f8;font-weight:600;display:flex;align-items:center}
.mb{padding:12px 16px;overflow:auto}
pre{background:#0f1b2e;color:#cfe3ff;padding:12px;border-radius:8px;font-size:12px;overflow:auto;max-height:60vh}
.note{color:#7a8699;font-size:12px;margin-top:10px;line-height:1.9}
</style></head><body>
<div class="top">
  <span class="logo">BIORI BIOTECH <small>客户特殊要求 · 候选复核</small></span>
  <span class="grow"></span>
  <button class="btn" onclick="pickAll('y')">全部采纳</button>
  <button class="btn" onclick="pickAll('n')">全部驳回</button>
  <button class="btn" onclick="resetPick()">清空标记</button>
  <button class="btn" onclick="exportCsv()">📥 导出 CSV</button>
  <button class="btn pri" onclick="exportJson()">💾 导出审核结果 JSON</button>
  <button class="btn" onclick="showRaw()">📋 原始数据</button>
</div>
<div class="wrap">
  <div class="kpis" id="kpis"></div>
  <div class="card">
    <div class="bar">
      <input type="text" id="q" placeholder="搜索：客户 / 标题 / 内容 / 货号 / 依据…" oninput="render()">
      <select id="fcat" onchange="render()"><option value="">全部类别</option></select>
      <select id="fconf" onchange="render()"><option value="">全部置信度</option><option>高</option><option>中</option><option>低</option></select>
      <select id="fchk" onchange="render()"><option value="">全部校验状态</option><option value="ok">✓ 通过校验</option><option value="bad">⚠ 有问题</option></select>
      <select id="fpick" onchange="render()"><option value="">全部标记</option><option value="y">已采纳</option><option value="n">已驳回</option><option value="none">未标记</option></select>
      <span class="grow"></span>
      <span id="cnt" style="color:#7a8699;font-size:13px"></span>
    </div>
    <div style="overflow:auto;max-height:70vh">
      <table>
        <thead><tr>
          <th onclick="sortBy('customer')">客户</th>
          <th onclick="sortBy('category')">类别</th>
          <th onclick="sortBy('title')">标题</th>
          <th class="no">内容（含依据原文）</th>
          <th class="no">涉及货号</th>
          <th onclick="sortBy('confidence')">置信度</th>
          <th onclick="sortBy('activity_date')">单据日期</th>
          <th onclick="sortBy('activity_owner')">销售员</th>
          <th onclick="sortBy('ok')">校验</th>
          <th class="no">操作</th>
        </tr></thead>
        <tbody id="tb"></tbody>
      </table>
    </div>
    <div class="note" id="foot"></div>
  </div>
</div>
<div class="modal" id="modal" onclick="if(event.target===this)hideRaw()">
  <div class="mbox"><div class="mh">📋 原始数据（数据/sr_review.json）<span class="grow" style="flex:1"></span><button class="btn" onclick="hideRaw()">关闭</button></div>
  <div class="mb"><pre id="rawbox">加载中…</pre></div></div>
</div>
<script>__APP_DATA__</script>
<script>
var D = window.__SR__ || { rows: [] };
var ROWS = D.rows || [];
var MARK = {};
try { MARK = JSON.parse(localStorage.getItem('sr_mark_v1') || '{}'); } catch (e) { MARK = {}; }
var SK = { key: '', asc: true };

function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
function saveMark() { try { localStorage.setItem('sr_mark_v1', JSON.stringify(MARK)); } catch (e) {} }
function pick(id, v) { MARK[id] = (MARK[id] === v ? '' : v); saveMark(); render(); }
function pickAll(v) { filtered().forEach(function (r) { MARK[r._id] = v; }); saveMark(); render(); }
function resetPick() { if (!confirm('清空所有采纳/驳回标记？')) return; MARK = {}; saveMark(); render(); }
function sortBy(k) { if (SK.key === k) { SK.asc = !SK.asc; } else { SK.key = k; SK.asc = true; } render(); }

function filtered() {
  var q = (document.getElementById('q').value || '').toLowerCase();
  var fc = document.getElementById('fcat').value, fconf = document.getElementById('fconf').value;
  var fchk = document.getElementById('fchk').value, fp = document.getElementById('fpick').value;
  var out = ROWS.filter(function (r) {
    if (fc && r.category !== fc) return false;
    if (fconf && r.confidence !== fconf) return false;
    if (fchk === 'ok' && !r.ok) return false;
    if (fchk === 'bad' && r.ok) return false;
    var m = MARK[r._id] || '';
    if (fp === 'y' && m !== 'y') return false;
    if (fp === 'n' && m !== 'n') return false;
    if (fp === 'none' && m) return false;
    if (q) {
      var blob = [r.customer, r.category, r.title, r.content, r.evidence, (r.skus || []).join(' '), r.activity_owner].join(' ').toLowerCase();
      if (blob.indexOf(q) < 0) return false;
    }
    return true;
  });
  if (SK.key) {
    out.sort(function (a, b) {
      var x = a[SK.key], y = b[SK.key];
      if (typeof x === 'boolean') { x = x ? 1 : 0; y = y ? 1 : 0; }
      x = String(x == null ? '' : x); y = String(y == null ? '' : y);
      return SK.asc ? x.localeCompare(y, 'zh') : y.localeCompare(x, 'zh');
    });
  }
  return out;
}

function render() {
  var rows = filtered(), tb = document.getElementById('tb');
  document.getElementById('cnt').textContent = '显示 ' + rows.length + ' / ' + ROWS.length + ' 条';
  tb.innerHTML = rows.map(function (r) {
    var m = MARK[r._id] || '';
    return '<tr class="' + (m === 'y' ? 'y' : (m === 'n' ? 'n' : '')) + '">' +
      '<td class="c">' + esc(r.customer) + '</td>' +
      '<td class="c"><span class="tag cat">' + esc(r.category) + '</span></td>' +
      '<td>' + esc(r.title) + '</td>' +
      '<td class="cont">' + esc(r.content) + (r.evidence ? '<div class="ev">依据：' + esc(r.evidence) + '</div>' : '') +
        (r.problems && r.problems.length ? '<div class="ev" style="color:#a32020">⚠ ' + esc(r.problems.join('、')) + '</div>' : '') + '</td>' +
      '<td class="c">' + esc((r.skus || []).join(' ')) + '</td>' +
      '<td class="c"><span class="tag t-' + esc(r.confidence) + '">' + esc(r.confidence) + '</span></td>' +
      '<td class="c">' + esc(r.activity_date) + '</td>' +
      '<td class="c">' + esc(r.activity_owner) + '</td>' +
      '<td class="c">' + (r.ok ? '<span class="tag ok">✓</span>' : '<span class="tag bad">⚠</span>') + '</td>' +
      '<td class="c"><button class="pick ' + (m === 'y' ? 'on y' : '') + '" onclick="pick(\\'' + r._id + '\\',\\'y\\')">采纳</button>' +
      '<button class="pick ' + (m === 'n' ? 'on n' : '') + '" onclick="pick(\\'' + r._id + '\\',\\'n\\')">驳回</button></td></tr>';
  }).join('') || '<tr><td colspan="10" style="text-align:center;color:#7a8699;padding:30px">无匹配数据</td></tr>';
  var y = 0, n = 0;
  ROWS.forEach(function (r) { var v = MARK[r._id]; if (v === 'y') y++; else if (v === 'n') n++; });
  document.getElementById('foot').innerHTML = '来源：' + esc(D.source || '') + ' ｜ 生成时间：' + esc(D.generated || '') +
    ' ｜ 已采纳 <b>' + y + '</b> 条 · 已驳回 <b>' + n + '</b> 条 · 未标记 ' + (ROWS.length - y - n) + ' 条' +
    '<br>操作：给每条点「采纳 / 驳回」→ 点右上角「💾 导出审核结果 JSON」发我，我按你的标记合并入库（采纳的写入正式库，驳回的不入库）。标记会自动保存在本机浏览器。';
}

function exportJson() {
  var rows = ROWS.map(function (r) {
    var o = JSON.parse(JSON.stringify(r));
    o.adopt = MARK[r._id] === 'y';
    o.reject = MARK[r._id] === 'n';
    return o;
  });
  var blob = new Blob([JSON.stringify({ source: D.source, exported: new Date().toISOString(), rows: rows }, null, 1)], { type: 'application/json' });
  var a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'sr_review_result_' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a); a.click(); setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 800);
}
function exportCsv() {
  var rows = filtered();
  var head = ['客户', '类别', '标题', '内容', '涉及货号', '依据', '置信度', '活动日期', '销售员', '校验', '标记'];
  var lines = [head.join(',')];
  rows.forEach(function (r) {
    var m = MARK[r._id] === 'y' ? '采纳' : (MARK[r._id] === 'n' ? '驳回' : '');
    lines.push([r.customer, r.category, r.title, r.content, (r.skus || []).join(' '), r.evidence, r.confidence, r.activity_date, r.activity_owner, r.ok ? '通过' : '有问题', m]
      .map(function (v) { return '"' + String(v == null ? '' : v).replace(/"/g, '""') + '"'; }).join(','));
  });
  var blob = new Blob(['\\ufeff' + lines.join('\\r\\n')], { type: 'text/csv;charset=utf-8' });
  var a = document.createElement('a');
  a.href = URL.createObjectURL(blob); a.download = 'sr_review_' + new Date().toISOString().slice(0, 10) + '.csv';
  document.body.appendChild(a); a.click(); setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 800);
}
function showRaw() {
  document.getElementById('modal').classList.add('on');
  document.getElementById('rawbox').textContent = JSON.stringify(D, null, 1).slice(0, 400000);
}
function hideRaw() { document.getElementById('modal').classList.remove('on'); }

(function init() {
  var cats = {}, conf = {};
  ROWS.forEach(function (r) { cats[r.category] = 1; conf[r.confidence] = 1; });
  var sel = document.getElementById('fcat');
  Object.keys(cats).sort().forEach(function (c) { var o = document.createElement('option'); o.value = c; o.textContent = c; sel.appendChild(o); });
  var clean = ROWS.filter(function (r) { return r.ok; }).length;
  var k = [
    ['候选总数', ROWS.length], ['通过校验', clean], ['有问题', ROWS.length - clean],
    ['涉及客户', D.by_customer || 0], ['原始抽出', D.total_raw || 0],
    ['高置信度', ROWS.filter(function (r) { return r.confidence === '高'; }).length]
  ];
  document.getElementById('kpis').innerHTML = k.map(function (p) {
    return '<div class="kpi"><div class="v">' + p[1] + '</div><div class="l">' + p[0] + '</div></div>';
  }).join('');
  render();
})();
</script></body></html>
"""


def main():
    global SRC, OUT
    # 支持换输入/输出（大客户专版等）：SR_REVIEW_JSON / SR_OUT / SR_TITLE
    SRC = os.environ.get('SR_REVIEW_JSON') or SRC
    OUT = os.environ.get('SR_OUT') or OUT
    title = os.environ.get('SR_TITLE') or '客户特殊要求 · 候选复核'
    if not os.path.exists(SRC):
        print('  ⚠️ 未找到 %s — 先跑 merge_special_requirements.py --review' % SRC)
        return 1
    d = json.load(open(SRC, encoding='utf-8'))
    rows = []
    for i, r in enumerate(d.get('rows') or []):
        r = dict(r)
        r['_id'] = str(i + 1)
        rows.append(r)
    payload = {
        'source': d.get('source') or '',
        'generated': d.get('generated') or '',
        'by_customer': d.get('by_customer') or 0,
        'total_raw': d.get('total_raw') or 0,
        'rows': rows,
    }
    html = TPL.replace('__APP_DATA__', 'window.__SR__=' + json.dumps(payload, ensure_ascii=False) + ';')
    if title != '客户特殊要求 · 候选复核':
        html = html.replace('<title>客户特殊要求 · 候选复核</title>', '<title>%s</title>' % title)
        html = html.replace('客户特殊要求 · 候选复核', title)
    os.makedirs(os.path.dirname(OUT), exist_ok=True)
    open(OUT, 'w', encoding='utf-8').write(html)
    # JS 语法校验
    js = '\n'.join(re.findall(r'<script>(.*?)</script>', html, re.S))
    p = '/tmp/_sr_check.js'
    open(p, 'w', encoding='utf-8').write(js)
    r = subprocess.run(['node', '--check', p], capture_output=True, text=True)
    print('  ✅ 生成 %s（%.0f KB，%d 行）｜ JS %s %s' % (OUT, len(html.encode()) / 1024, len(rows),
          '✅' if r.returncode == 0 else '❌', r.stderr[:200] if r.returncode else ''))
    return 0


if __name__ == '__main__':
    sys.exit(main())
