#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""三部门逐月拆分_demo.html —— 行级数据授权（WORKBENCH_AUTH_V1）+ 重新注入最新 JSON。

单一取数入口 visibleCustomers()：所有「客户名明细」在渲染前过滤。
红线：
  1) 非 admin/gm 只能看到 sales==本人姓名 的客户
  2) 无归属(sales=null)/保密客户 仅 admin/gm
  3) 集团口径(customers_grp) 仅 admin/gm；非管理员切换 → 提示 + 不出数
  4) 部门/月度聚合（矩阵/KPI）人人可见（不含客户名）
  5) 过滤在渲染前生效
"""
import json, io, os, re, sys

W = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PAGE = os.path.join(W, '工具/仪表盘/三部门逐月拆分_demo.html')
DATA = os.path.join(W, '数据/三部门逐月_2026.json')

s = io.open(PAGE, encoding='utf-8').read()
orig_len = len(s)

# ── 1. 重新注入最新 JSON（花括号配平扫描，不用正则）────────────────
anchor = 'window.__DATA__ = '
start = s.find(anchor)
assert start > 0, 'window.__DATA__ 锚点未找到'
start += len(anchor)
depth = 0; i = start; instr = False; esc = False; end = None
while i < len(s):
    ch = s[i]
    if instr:
        if esc: esc = False
        elif ch == '\\': esc = True
        elif ch == '"': instr = False
    else:
        if ch == '"': instr = True
        elif ch == '{': depth += 1
        elif ch == '}':
            depth -= 1
            if depth == 0:
                end = i + 1; break
    i += 1
assert end, '旧 JSON 结束位置未找到'
old_json_len = end - start

new_json = io.open(DATA, encoding='utf-8').read()
d = json.loads(new_json)
assert any('sales' in c for c in d['customers']), '注入源缺 sales 字段'
assert any('sales_list' in c for c in d['customers_grp']), '注入源缺 sales_list'
assert '</script' not in new_json.lower(), 'JSON 含 </script'
s = s[:start] + new_json + s[end:]
print('JSON 注入: old %d bytes -> new %d bytes' % (old_json_len, len(new_json)))

# ── 2. 授权提示条 CSS（插在 </head> 前）────────────────────────────
AUTH_CSS = """<style>
/* ===== 行级授权提示条（WORKBENCH_AUTH_V1）===== */
#authbar{margin:2px 0 12px;padding:9px 14px;border:1px solid #c9dcf3;border-radius:10px;
  background:#eef4fc;color:#17427c;font-size:12.8px;line-height:1.6;font-weight:600}
#authbar b{font-weight:800}
#authbar.warn{background:#fff4e5;border-color:#f0d4a8;color:#8a5a00}
#authbar.priv{background:#e8f5ee;border-color:#b7e0c6;color:#1d6b3f}
#authbar.grpdeny{background:#fdeeee;border-color:#f3c7c7;color:#a12626}
</style>
</head>"""
assert s.count('</head>') == 1
s = s.replace('</head>', AUTH_CSS, 1)

# ── 3. 授权提示条 HTML ────────────────────────────────────────────
assert s.count('<div class="wrap">') == 1
s = s.replace('<div class="wrap">\n  <div class="pagehead">',
              '<div class="wrap">\n  <div id="authbar" class="warn">身份识别中…</div>\n  <div class="pagehead">', 1)

# ── 4. 授权 JS：单一取数入口 ──────────────────────────────────────
AUTH_JS = r"""
/* ================= 行级数据授权（WORKBENCH_AUTH_V1） ================= */
/* 身份来源：① 工作台 iframe 传入的 URL 参数 u/r/d ② 工作台 localStorage 登录快照 bior_sw_user_v2。
   两者都拿不到 → 视为「未登录」，按普通销售且无可见客户处理（宁可少显，不可泄露）。 */
var WB_ROLE_KEY = 'bior_sw_user_v2';
function _qsv(k){
  try{
    var m = new RegExp('[?&]' + k + '=([^&]*)').exec(window.location.search || '');
    return m ? decodeURIComponent(m[1].replace(/\+/g, ' ')) : '';
  }catch(e){ return ''; }
}
var AUTH = (function(){
  var name = '', role = '', dept = '', src = '';
  var qu = _qsv('u'), qr = _qsv('r'), qd = _qsv('d');
  if(qu){ name = qu; role = qr || 'sales'; dept = qd || ''; src = 'workbench-url'; }
  else {
    try{
      var raw = window.localStorage ? window.localStorage.getItem(WB_ROLE_KEY) : null;
      if(raw){ var o = JSON.parse(raw); if(o && o.name){ name = o.name; role = o.role || 'sales'; dept = o.dept || ''; src = 'workbench-session'; } }
    }catch(e){}
  }
  return {name:name, role:role || 'sales', dept:dept, loggedIn:!!name, src:src};
})();
function authPriv(){ return AUTH.role === 'admin' || AUTH.role === 'gm'; }
function authRoleLabel(){
  var r = AUTH.role;
  if(r === 'admin') return '管理员';
  if(r === 'gm') return '总经理';
  if(r === 'manager') return '经理';
  return '销售';
}

/* ★ 单一取数入口：所有「客户名明细」必须走这里（渲染前过滤，不做事后隐藏） */
function visibleCustomers(){
  var arr = (D && Object.prototype.toString.call(D.customers) === '[object Array]') ? D.customers : [];
  if(authPriv()) return arr.slice();                       /* 红线1/2：admin/gm 全部 */
  if(CUSTMODE === 'grp') return [];                        /* 红线3：集团口径仅 admin/gm */
  if(!AUTH.name) return [];                                /* 红线2：未登录 → 0 行 */
  var nm = AUTH.name, out = [];
  for(var i = 0; i < arr.length; i++){
    var r = arr[i]; if(!r) continue;
    var sv = r.sales;
    if(sv == null || sv === '') continue;                  /* 红线2：无归属/保密客户仅 admin/gm */
    if(Object.prototype.toString.call(sv) === '[object Array]'){ if(sv.indexOf(nm) >= 0) out.push(r); }
    else if(sv === nm) out.push(r);
  }
  return out;
}
function visibleStats(){
  var rows = visibleCustomers(), seen = {}, n = 0, amt = 0;
  for(var i = 0; i < rows.length; i++){
    var r = rows[i]; if(!r) continue;
    if(MONTHS.length && MONTHS.indexOf(r.month) < 0) continue;
    var key = (r.cust == null || r.cust === '') ? '（未标注）' : String(r.cust);
    if(!seen[key]){ seen[key] = 1; n++; }
    var a = N(r.amt); if(isNum(a)) amt += a;
  }
  return {n:n, amt:amt};
}
function renderAuthBar(){
  var el = document.getElementById('authbar'); if(!el) return;
  var pl = (PERIOD === 'q3') ? 'Q3' : ('1-' + MONTHS.length + '月');
  if(CUSTMODE === 'grp' && !authPriv()){
    el.className = 'grpdeny';
    el.innerHTML = '⛔ <b>集团口径不可见</b>：集团会跨销售员归集（易造出「假集团」），仅管理员 / 总经理可见。' +
      '当前身份：' + esc(AUTH.name || '未登录') + '（' + esc(authRoleLabel()) + '）｜ 客户名相关面板一律不出数。';
    return;
  }
  if(authPriv()){
    var st0 = visibleStats();
    el.className = 'priv';
    el.innerHTML = '🔓 <b>当前身份：' + esc(AUTH.name || '—') + '（' + esc(authRoleLabel()) + '）</b> ｜ 可见范围：' +
      '<b>全部客户 ' + st0.n + ' 家</b> ｜ 可见金额 <b>' + fmt(st0.amt) + '</b> 万元 ｜ 含无归属与保密客户、集团口径（' + pl + '）';
    return;
  }
  if(!AUTH.loggedIn){
    el.className = 'warn';
    el.innerHTML = '⚠️ <b>未登录</b>：未取得工作台登录态，按普通销售处理且 <b>无可见客户</b>。' +
      '为避免泄露，客户名明细一律不显示（仅显示部门 / 月度聚合视图）。';
    return;
  }
  var st = visibleStats();
  el.className = '';
  el.innerHTML = '🔒 <b>当前身份：' + esc(AUTH.name) + '（' + esc(authRoleLabel()) + '）</b> ｜ 可见客户 <b>' + st.n +
    '</b> 家 ｜ 可见金额 <b>' + fmt(st.amt) + '</b> 万元（' + pl + '）｜ 无归属 / 保密客户与集团口径不可见';
}
function applyAuthVisibility(){
  var sn = document.getElementById('secret-note');
  if(sn) sn.style.display = authPriv() ? '' : 'none';
  renderAuthBar();
}
"""

anchor2 = "var PERIOD = 'all';\n"
assert s.count(anchor2) == 1, 'PERIOD 锚点异常: %d' % s.count(anchor2)
s = s.replace(anchor2, anchor2 + AUTH_JS, 1)

# ── 5. 三个客户名取数处统一走 visibleCustomers() ──────────────────
a = "function custAgg(){\n  var map = {};\n  (D.customers || []).forEach(function(r){"
b = "function custAgg(){\n  var map = {};\n  visibleCustomers().forEach(function(r){"
assert s.count(a) == 1, 'custAgg 锚点 %d' % s.count(a)
s = s.replace(a, b, 1)

a = "function custBridge(){\n  var map = {};\n  (D.customers || []).forEach(function(r){"
b = "function custBridge(){\n  var map = {};\n  visibleCustomers().forEach(function(r){"
assert s.count(a) == 1, 'custBridge 锚点 %d' % s.count(a)
s = s.replace(a, b, 1)

a = "var src = (mode === 'cust') ? (D.customers || []) : (D.salespersons || []);"
b = "var src = (mode === 'cust') ? visibleCustomers() : (D.salespersons || []);"
assert s.count(a) == 1, 'renderDrill 锚点 %d' % s.count(a)
s = s.replace(a, b, 1)

# 驱动页 hasCust：以可见客户为准（非管理员/未登录时视为无客户）
a = "var hasCust = (D.customers || []).length > 0;"
b = "var hasCust = visibleCustomers().length > 0;"
assert s.count(a) == 1
s = s.replace(a, b, 1)

# ── 6. 「保密客户」说明文案 → 仅 admin/gm 可见 ─────────────────────
a = '<div class="cap" style="display:block;margin-top:9px;line-height:1.7;text-align:left">注：「保密客户」'
b = '<div class="cap" id="secret-note" style="display:block;margin-top:9px;line-height:1.7;text-align:left">注：「保密客户」'
assert s.count(a) == 1
s = s.replace(a, b, 1)

# ── 7. custModeApply：集团口径非管理员 → 提示且不出数 ──────────────
a = """    if(CUSTMODE === 'grp'){
      var gg = {}; (D.customers_grp || []).forEach(function(r){ if(r && r.grp) gg[r.grp] = 1; });
      nt.textContent = '集团口径：已归集 ' + Object.keys(gg).length + ' 个集团（源：官方《诊断原料&生命科学客户清单》的「集团简称→成员客户」映射，落盘于 数据/diag_mappings.json）；无集团归属的客户保持独立实体统计。仅合并、不丢弃 —— 与实体口径合计数完全一致。';
    } else {
      nt.textContent = '实体口径：按 ERP 客户名统计，同一集团可能被拆成多行。';
    }"""
b = """    if(CUSTMODE === 'grp'){
      if(!authPriv()){
        nt.textContent = '⛔ 集团口径仅管理员 / 总经理可见：集团会跨销售员归集（按单人过滤会造出「只属于我的假集团」）。当前身份无权查看集团数据，客户名相关面板一律不出数。';
      } else {
        var gg = {}; (D.customers_grp || []).forEach(function(r){ if(r && r.grp) gg[r.grp] = 1; });
        nt.textContent = '集团口径：已归集 ' + Object.keys(gg).length + ' 个集团（源：官方《诊断原料&生命科学客户清单》的「集团简称→成员客户」映射，落盘于 数据/diag_mappings.json）；无集团归属的客户保持独立实体统计。仅合并、不丢弃 —— 与实体口径合计数完全一致。';
      }
    } else {
      nt.textContent = '实体口径：按 ERP 客户名统计，同一集团可能被拆成多行。' +
        (authPriv() ? '' : '（当前身份仅可见归属本人的客户；无归属 / 保密客户不可见）');
    }"""
assert s.count(a) == 1, 'custModeApply 锚点 %d' % s.count(a)
s = s.replace(a, b, 1)

# custModeApply 末尾追加授权刷新
a = """  try{ if(typeof resizeCharts === 'function') setTimeout(resizeCharts, 60); }catch(e){}
}

/* ===== 期间切换：全期(1-9月) / Q3(7-9月) ===== */"""
b = """  try{ if(typeof applyAuthVisibility === 'function') applyAuthVisibility(); }catch(e){}
  try{ if(typeof resizeCharts === 'function') setTimeout(resizeCharts, 60); }catch(e){}
}

/* ===== 期间切换：全期(1-9月) / Q3(7-9月) ===== */"""
assert s.count(a) == 1, 'custModeApply 尾部锚点 %d' % s.count(a)
s = s.replace(a, b, 1)

# ── 8. periodApply 末尾 + init() 追加授权刷新 ─────────────────────
a = """  try{ buildFilters(); initDrill(); }catch(e){}
  try{ setTimeout(resizeCharts, 60); }catch(e){}
}"""
b = """  try{ buildFilters(); initDrill(); }catch(e){}
  try{ if(typeof applyAuthVisibility === 'function') applyAuthVisibility(); }catch(e){}
  try{ setTimeout(resizeCharts, 60); }catch(e){}
}"""
assert s.count(a) == 1, 'periodApply 尾部锚点 %d' % s.count(a)
s = s.replace(a, b, 1)

a = """function init(){
  updatePageSub();
  bindTabs();"""
b = """function init(){
  updatePageSub();
  applyAuthVisibility();
  bindTabs();"""
assert s.count(a) == 1, 'init 锚点 %d' % s.count(a)
s = s.replace(a, b, 1)

io.open(PAGE, 'w', encoding='utf-8').write(s)
print('done. %d -> %d chars' % (orig_len, len(s)))
print('</body> count =', s.count('</body>'))
print('<script count =', len(re.findall(r'<script', s)), '</script> count =', s.count('</script>'))
