#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
客户 Tab ·「💰 价格查询」面板 + 年份切换 补丁脚本
=====================================================
给 工具/index.html 与 工具/app.html 注入可折叠的「💰 价格查询」面板（放在客户透视卡之后、
客户清单之前），面板内带「年份」切换：全部 / 2024 / 2025 / 2026（默认全部）。

为什么年份切换走服务端：价格数据权限必须服务端强制。前端把 year 作为参数传给
GET /api/price/query?sku=..&year=..，服务端过滤后再算区间/明细，前端只做展示，
不改动任何隔离口径。

安全约定（沿用工程铁律）：
  · 每个 replace 之前 assert 锚点「存在且唯一」，不唯一直接 abort（不写盘）
  · 幂等：已注入过（存在 renderPricePanel 定义）则跳过
  · 写盘前先备份 <file>.bak_priceyear_<ts>
  · 写完抽取全部内联 <script> 交给 node --check 做语法校验，任一失败则回滚

用法：/usr/bin/python3 脚本/patch_price_query_year.py [--dry-run]
"""

import argparse
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FILES = ['工具/index.html', '工具/app.html']

# 锚点：renderCustomers 里「客户清单」卡片那一行（两个文件各只出现 1 次）
ANCHOR = ("'<div class=\"card\"><div class=\"card-header\"><h3>📋 客户清单 "
          "<span class=\"tag tag-gray\">' + filtered.length + '</span></h3></div>' +")

MARKER = 'function renderPricePanel('


# ────────────────────────── 注入的 JS ──────────────────────────
PANEL_JS = r'''
/* ===== 💰 价格查询（ERP 真实成交价 · 服务端权限隔离 · 年份切换 2024/2025/2026） =====
   数据来自 GET /api/price/query?sku=<关键词>&year=<all|2024|2025|2026>
   · 权限在服务端强制：普通销售只拿到「销售员==本人」的明细 + 全库匿名区间；
     admin/gm（或被授权「全量」的人）才拿全量明细。前端拿不到别人的客户名。
   · 同一货号可能同时以 U / ml / 瓶 成交，跨单位比价无意义，面板会按单位分开提示。
   ================================================================ */
var priceQueryYear = 'all';
var priceQueryKw = '';
var priceQueryData = null;
var priceQueryLoading = false;
var priceQueryErr = '';
var pricePanelOpen = true;
var priceQueryHotTried = false;
var PRICE_YEAR_FALLBACK = [{year: 'all', label: '全部'}, {year: '2024', label: '2024'},
                           {year: '2025', label: '2025'}, {year: '2026', label: '2026'}];

function priceQueryFmtPrice(v) {
  if (v === null || v === undefined || v === '') return '—';
  var n = Number(v);
  if (!isFinite(n)) return String(v);
  if (n === 0) return '0';
  var a = Math.abs(n);
  if (a >= 1000) return n.toFixed(2);
  if (a >= 1) return n.toFixed(4).replace(/0+$/, '').replace(/\.$/, '');
  if (a >= 0.0001) return n.toFixed(6).replace(/0+$/, '').replace(/\.$/, '');
  return n.toExponential(2);
}

function priceQueryGo(kw, yr) {
  if (kw !== undefined && kw !== null) priceQueryKw = String(kw);
  if (yr) priceQueryYear = yr;
  priceQueryKw = (priceQueryKw || '').replace(/^\s+|\s+$/g, '');
  pricePanelOpen = true;
  priceQueryLoading = true;
  priceQueryErr = '';
  render();
  var base = (typeof AUTH_APIS !== 'undefined' && AUTH_APIS.length) ? AUTH_APIS[0] : '';
  var url = (base || '') + '/api/price/query?sku=' + encodeURIComponent(priceQueryKw) +
            '&year=' + encodeURIComponent(priceQueryYear);
  apiFetch(url).then(function(r) { return r.json(); }).then(function(d) {
    priceQueryLoading = false;
    if (!d || !d.ok) {
      priceQueryErr = (d && d.error) ? d.error : '查询失败';
      priceQueryData = null;
    } else {
      priceQueryData = d;
    }
    render();
  }).catch(function(e) {
    priceQueryLoading = false;
    priceQueryErr = (e && e.message === 'auth_expired') ? '登录已过期，请重新登录'
                  : ('请求失败：' + ((e && e.message) ? e.message : e));
    priceQueryData = null;
    render();
  });
}

function priceQueryYearSet(y) {
  priceQueryYear = y;
  priceQueryGo(priceQueryKw, y);
}

function priceQueryHotChip(sku) {
  return '<span onclick="priceQueryGo(\'' + esc(sku) + '\')" style="cursor:pointer;font-size:12px;' +
         'padding:3px 9px;border-radius:11px;border:1px solid #d5dbe6;background:#fff;margin:0 6px 6px 0;' +
         'display:inline-block">' + esc(sku) + '</span>';
}

function priceQueryYearTable(byYear, units) {
  if (!byYear) return '';
  var ys = ['2024', '2025', '2026'];
  var any = false, rows = '', prev = null;
  for (var i = 0; i < ys.length; i++) {
    var y = ys[i], a = byYear[y];
    if (!a) { continue; }
    any = true;
    var trend = '—';
    if (prev !== null && prev > 0 && a.avg > 0) {
      var pc = (a.avg - prev) / prev * 100;
      trend = '<span style="color:' + (pc >= 0 ? '#c0392b' : '#0d904f') + ';font-weight:700">' +
              (pc >= 0 ? '▲ +' : '▼ ') + pc.toFixed(1) + '%</span>';
    }
    rows += '<tr><td><b>' + y + '</b></td><td class="num">' + a.deal_count + '</td>' +
            '<td class="num">' + priceQueryFmtPrice(a.avg) + '</td>' +
            '<td class="num">' + priceQueryFmtPrice(a.min) + '</td>' +
            '<td class="num">' + priceQueryFmtPrice(a.max) + '</td>' +
            '<td class="num">' + (a.customer_count || 0) + '</td><td class="num">' + trend + '</td></tr>';
    prev = a.avg;
  }
  if (!any) return '<div style="color:var(--sub);font-size:12px">所选年份该货号无成交记录。</div>';
  var warn = '';
  if (units && units.length > 1) {
    warn = '<div style="background:#fff8e1;border-left:3px solid #f0ad4e;padding:6px 10px;font-size:11.5px;' +
           'color:#7a5c00;margin:6px 0">⚠️ 该货号存在多种销售单位（' + esc(units.join(' / ')) +
           '），跨单位的均价/最低/最高没有可比性；请按下方「按单位」表在同一单位内比价。</div>';
  } else {
    warn = '<div style="font-size:11px;color:var(--sub);margin:4px 0">趋势列为该年算术均相对上一「有成交年份」的变化；单位一致时才可比。</div>';
  }
  return warn + '<table class="data-table"><thead><tr><th>年份</th><th class="num">成交次数</th>' +
         '<th class="num">均价</th><th class="num">最低</th><th class="num">最高</th>' +
         '<th class="num">客户数</th><th class="num">同比</th></tr></thead><tbody>' + rows + '</tbody></table>';
}

function priceQueryUnitTable(byUnit) {
  if (!byUnit) return '';
  var keys = [], k;
  for (k in byUnit) { if (Object.prototype.hasOwnProperty.call(byUnit, k)) keys.push(k); }
  if (keys.length < 2) return '';
  var rows = '';
  keys.sort();
  for (var i = 0; i < keys.length; i++) {
    var u = keys[i], a = byUnit[u];
    var by = '';
    var yy = ['2024', '2025', '2026'], parts = [];
    for (var j = 0; j < yy.length; j++) {
      var ya = (a.by_year || {})[yy[j]];
      if (ya) parts.push(yy[j] + '：均' + priceQueryFmtPrice(ya.avg) + '（' + ya.deal_count + '次）');
    }
    by = parts.join('　');
    rows += '<tr><td><b>' + esc(u) + '</b></td><td class="num">' + a.deal_count + '</td>' +
            '<td class="num">' + priceQueryFmtPrice(a.p50) + '</td>' +
            '<td class="num">' + priceQueryFmtPrice(a.min) + ' ~ ' + priceQueryFmtPrice(a.max) + '</td>' +
            '<td style="font-size:11px">' + by + '</td></tr>';
  }
  return '<div class="card-header" style="padding:8px 0 4px"><h3 style="font-size:13px">按单位（务必同单位比价）</h3></div>' +
         '<table class="data-table"><thead><tr><th>单位</th><th class="num">成交次数</th>' +
         '<th class="num">中位价</th><th class="num">区间</th><th>分年</th></tr></thead><tbody>' +
         rows + '</tbody></table>';
}

function renderPricePanel() {
  if (state.user && state.user.price_scope === 'none') return '';
  var d = priceQueryData;
  var yopts = (d && d.year_options) ? d.year_options : PRICE_YEAR_FALLBACK;
  var scopeTag = d ? ('<span class="tag" style="background:#eef2ff;color:#1852B6">' +
                      esc(d.scope_label || d.scope) + '</span>') : '';
  var h = '<div class="card" style="margin-bottom:16px"><div class="card-header" style="flex-wrap:wrap;gap:6px">' +
    '<h3>💰 价格查询 <span style="font-size:11px;color:var(--sub);font-weight:400">ERP 真实成交价 · 含税 · 已排赠品与预测单</span></h3>' +
    scopeTag +
    '<span style="flex:1"></span>' +
    '<button class="btn btn-sm" onclick="pricePanelOpen=!pricePanelOpen;render()">' +
    (pricePanelOpen ? '收起 ▲' : '展开 ▼') + '</button></div>';
  if (!pricePanelOpen) { return h + '</div>'; }
  h += '<div class="card-body">';
  h += '<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px">' +
    '<input class="input" id="priceKw" style="flex:1;min-width:220px" autocomplete="off" ' +
    'placeholder="输入货号（如 FE13）或产品名关键词（如 Taq）" value="' + esc(priceQueryKw) + '" ' +
    'onkeydown="if(event.keyCode===13){priceQueryGo(this.value)}">' +
    '<button class="btn btn-sm" onclick="priceQueryGo(document.getElementById(\'priceKw\').value)">查询</button>' +
    '</div>';
  h += '<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-bottom:10px">' +
       '<span style="font-size:11px;color:var(--sub)">年份</span>';
  for (var i = 0; i < yopts.length; i++) {
    var o = yopts[i], act = (String(o.year) === String(priceQueryYear));
    h += '<span onclick="priceQueryYearSet(\'' + esc(String(o.year)) + '\')" style="cursor:pointer;' +
         'font-size:12px;padding:3px 10px;border-radius:12px;border:1px solid ' +
         (act ? 'var(--pri)' : '#d5dbe6') + ';background:' + (act ? 'var(--pri)' : '#fff') +
         ';color:' + (act ? '#fff' : '#374151') + '">' + esc(o.label || o.year) +
         ((o.record_count !== null && o.record_count !== undefined) ? ' <b>' + o.record_count + '</b>' : '') +
         '</span>';
  }
  h += '<span style="font-size:11px;color:var(--sub);margin-left:6px">（只影响下方明细与区间，口径与索引一致）</span></div>';

  if (priceQueryErr) {
    h += '<div style="background:#fdecea;border-left:3px solid #d93025;padding:8px 12px;font-size:12px;' +
         'color:#8a1c14">' + esc(priceQueryErr) + '</div>';
  }
  if (priceQueryLoading) {
    h += '<div style="color:var(--sub);font-size:12px;padding:10px 0">查询中…</div>';
  } else if (!priceQueryData) {
    if (!priceQueryHotTried) {
      priceQueryHotTried = true;
      setTimeout(function() { priceQueryGo(''); }, 0);
    }
    h += '<div style="color:var(--sub);font-size:12px;padding:6px 0">输入货号或产品名关键词开始查询；下方为成交次数最多的货号。</div>';
  } else if (d && d.found) {
    var p = d.product || {}, mk = d.market || {}, mine = d.mine;
    h += '<div style="background:#f6f8fc;border-radius:8px;padding:10px 12px;margin-bottom:10px">' +
      '<div style="font-size:15px;font-weight:800;color:#1852B6">' + esc(p.sku || d.sku || '') + '　' + esc(p.name || '') + '</div>' +
      '<div style="font-size:12px;color:#374151;margin-top:4px">规格：' + esc(p.spec || '—') +
      '　品类：' + esc(p.cat || '—') + '　有效期：' + (p.life_months ? p.life_months + ' 个月' : '—') + '</div>' +
      (p.unlisted ? '<div style="font-size:11px;color:#c0392b;margin-top:4px">⚠️ 该货号未收录进产品主数据（多为组分码/非分子线物料），仅按 ERP 物料名称展示，勿据此判断产品归属。</div>' : '') +
      '</div>';
    h += '<div class="card-header" style="padding:0 0 4px"><h3 style="font-size:13px">年度分布（' +
         (d.year === 'all' ? '三年合计' : esc(d.year) + ' 年') + '）</h3></div>';
    h += priceQueryYearTable(d.sku_by_year, d.units);
    h += priceQueryUnitTable(d.by_unit);
    if (mk && mk.deal_count) {
      h += '<div class="card-header" style="padding:10px 0 4px"><h3 style="font-size:13px">全库区间（不点名其他客户）' +
           '<span style="font-size:11px;color:var(--sub);font-weight:400">　样本 ' + mk.deal_count +
           ' 次 · ' + (mk.customer_count || 0) + ' 家客户 · 单位 ' + esc((mk.units || []).join('/') || '—') + '</span></h3></div>';
      h += '<div style="display:flex;gap:14px;flex-wrap:wrap;font-size:12.5px;margin-bottom:8px">' +
           '<span>最低 <b>' + priceQueryFmtPrice(mk.min) + '</b></span>' +
           '<span>P25 <b>' + priceQueryFmtPrice(mk.p25) + '</b></span>' +
           '<span>中位 <b>' + priceQueryFmtPrice(mk.p50) + '</b></span>' +
           '<span>P75 <b>' + priceQueryFmtPrice(mk.p75) + '</b></span>' +
           '<span>最高 <b>' + priceQueryFmtPrice(mk.max) + '</b></span>' +
           '<span style="color:var(--sub)">最近一次 ' + priceQueryFmtPrice((mk.last || {}).price) +
           '（' + esc((mk.last || {}).date || '—') + '）</span></div>';
    }
    if (d.compare && d.compare.vs_market_p50_pct !== null && d.compare.vs_market_p50_pct !== undefined) {
      var v = d.compare.vs_market_p50_pct;
      h += '<div style="font-size:12px;margin-bottom:8px">你的价格 vs 全库中位：' +
           '<b style="color:' + (v >= 0 ? '#c0392b' : '#0d904f') + '">' + (v >= 0 ? '+' : '') + v + '%</b>' +
           '<span style="color:var(--sub)">（负值＝低于全库中位，报价更有竞争力）</span></div>';
    }
    if (mine) {
      h += '<div style="font-size:11.5px;color:var(--sub);margin-bottom:6px">你可见的明细 ' + (mine.deal_count || 0) +
           ' 条 · 覆盖 ' + (mine.customer_count || 0) + ' 家客户 · 销货量 ' +
           priceQueryFmtPrice(mine.qty_total) + ' · 金额 ' + ((mine.amount_total || 0) / 1e4).toFixed(2) + ' 万</div>';
    }
    if (d.own_note) {
      h += '<div style="font-size:11px;color:var(--sub);margin-bottom:6px">' + esc(d.own_note) + '</div>';
    }
    var bc = d.by_customer || [];
    if (bc.length) {
      h += '<div class="card-header" style="padding:8px 0 4px"><h3 style="font-size:13px">按客户</h3></div>' +
           '<div style="max-height:320px;overflow:auto"><table class="data-table"><thead><tr>' +
           '<th>客户</th><th class="num">成交次数</th><th class="num">最近价</th>' +
           '<th class="num">区间</th><th class="num">销货量</th><th class="num">金额</th></tr></thead><tbody>';
      for (var ci = 0; ci < bc.length; ci++) {
        var r0 = bc[ci];
        h += '<tr><td class="truncate" style="max-width:220px" title="' + esc(r0.customer) + '">' +
             esc(r0.customer) + '</td><td class="num">' + r0.deal_count + '</td>' +
             '<td class="num"><b>' + priceQueryFmtPrice((r0.last || {}).price) + '</b></td>' +
             '<td class="num">' + priceQueryFmtPrice(r0.min) + ' ~ ' + priceQueryFmtPrice(r0.max) + '</td>' +
             '<td class="num">' + priceQueryFmtPrice(r0.qty_total) + '</td>' +
             '<td class="num">' + ((r0.amount_total || 0) / 1e4).toFixed(2) + '万</td></tr>';
      }
      h += '</tbody></table></div>';
    }
    var rs = d.records || [];
    if (rs.length) {
      h += '<div class="card-header" style="padding:10px 0 4px"><h3 style="font-size:13px">成交明细' +
           '<span style="font-size:11px;color:var(--sub);font-weight:400">　显示 ' + rs.length + ' / ' +
           (d.records_total || 0) + ' 条' + (d.truncated ? '（已截断）' : '') + '</span></h3></div>' +
           '<div style="max-height:340px;overflow:auto"><table class="data-table"><thead><tr>' +
           '<th>日期</th><th>客户</th>' + (d.scope === 'all' ? '<th>销售员</th>' : '') +
           '<th class="num">单位</th><th class="num">数量</th><th class="num">含税单价</th><th class="num">金额</th>' +
           '</tr></thead><tbody>';
      for (var ri = 0; ri < rs.length; ri++) {
        var r = rs[ri];
        h += '<tr><td>' + esc(r.date || '—') + '</td>' +
             '<td class="truncate" style="max-width:200px" title="' + esc(r.customer) + '">' + esc(r.customer) + '</td>' +
             (d.scope === 'all' ? '<td>' + esc(r.sales || '—') + '</td>' : '') +
             '<td class="num">' + esc(r.unit || '—') + '</td>' +
             '<td class="num">' + priceQueryFmtPrice(r.qty) + '</td>' +
             '<td class="num"><b>' + priceQueryFmtPrice(r.price) + '</b></td>' +
             '<td class="num">' + ((r.total || 0) / 1e4).toFixed(2) + '万</td></tr>';
      }
      h += '</tbody></table></div>';
    }
  } else if (d && d.matches && d.matches.length) {
    h += '<div style="font-size:12px;margin-bottom:6px">匹配到 ' + d.matches.length + ' 个货号，请点击选择：</div>';
    for (var mi2 = 0; mi2 < d.matches.length; mi2++) { h += priceQueryHotChip(d.matches[mi2].sku); }
  } else if (d && d.hot) {
    h += '<div style="font-size:12px;margin-bottom:6px;color:var(--sub)">' + esc(d.hint || '') + '</div>';
    for (var hi = 0; hi < d.hot.length; hi++) {
      h += priceQueryHotChip(d.hot[hi].sku);
    }
  } else if (d) {
    h += '<div style="color:var(--sub);font-size:12px;padding:6px 0">' + esc(d.hint || '没有匹配结果') + '</div>';
  }
  var disc = (d && d.disclaimer) ? d.disclaimer :
    '价格口径：ERP 含税单价 > 0，已排除赠品与预测单；同一货号可能同时以 U / ml / 瓶 成交，须同单位比价。';
  h += '<div style="font-size:10.5px;color:var(--sub);margin-top:8px;line-height:1.6">' + esc(disc) +
       '<br>权限：普通销售仅见本人客户明细 + 全库匿名区间；管理员/授权「全量」者见全量明细。数据仅存于本机服务，不外传。</div>';
  return h + '</div></div>';
}
'''


def js_check(path_js):
    """node --check 语法校验"""
    r = subprocess.run(['node', '--check', path_js], capture_output=True, text=True)
    return r.returncode == 0, (r.stderr or r.stdout).strip()


def inline_scripts(content):
    return [m.group(2) for m in re.finditer(r'<script\b([^>]*)>(.*?)</script>', content,
                                            re.S | re.I)
            if 'src=' not in m.group(1).lower()]


def patch_file(rel, dry=False):
    path = os.path.join(ROOT, rel)
    src = open(path, encoding='utf-8').read()
    if MARKER in src:
        print('  ⏭  %s 已注入过（存在 %s），跳过' % (rel, MARKER.strip()))
        return 'skip'
    n = src.count(ANCHOR)
    assert n == 1, '锚点不唯一/不存在（%s 命中 %d 次）：%s' % (rel, n, ANCHOR[:60])
    out = src.replace(ANCHOR, 'renderPricePanel() +\n    ' + ANCHOR, 1)
    assert out != src, '锚点替换未生效：%s' % rel
    idx = out.rindex('</script>')
    assert idx > 0, '找不到 </script>：%s' % rel
    out = out[:idx] + PANEL_JS + '\n' + out[idx:]
    assert out.count(MARKER) == 1, '注入后 renderPricePanel 出现次数异常：%s' % rel
    assert out.count('priceQueryYearSet') >= 2, '年份切换函数未注入：%s' % rel

    if dry:
        print('  🧪 %s 锚点 OK（dry-run 不写盘）' % rel)
        return 'dry'

    bak = path + '.bak_priceyear_' + time.strftime('%Y%m%d_%H%M%S')
    shutil.copy2(path, bak)

    # 先落到临时文件做 node --check，通过才覆盖（不通过则原始文件根本没动）
    tmpdir = tempfile.mkdtemp(prefix='priceyear_')
    try:
        for i, js in enumerate(inline_scripts(out)):
            pj = os.path.join(tmpdir, 'blk%d.js' % i)
            open(pj, 'w', encoding='utf-8').write(js)
            ok, err = js_check(pj)
            assert ok, '%s 内联 <script> #%d 语法错误：%s' % (rel, i + 1, err[:400])
        open(path, 'w', encoding='utf-8').write(out)
    finally:
        shutil.rmtree(tmpdir, ignore_errors=True)
    print('  ✅ %s 已注入（备份 %s，内联 JS node --check 通过）' % (rel, os.path.basename(bak)))
    return 'ok'


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--dry-run', action='store_true')
    a = ap.parse_args()
    print('客户 Tab · 💰 价格查询面板 + 年份切换 补丁')
    res = {}
    for rel in FILES:
        res[rel] = patch_file(rel, a.dry_run)
    print('汇总：%s' % res)
    return 0


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