#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""把「研发填写板块」注入 客户拉通大表 demo（第二层）。

口径（用户 2026-10-11 拍板）：
  1. 先加「研发填写板块」（不做独立第三层页）；
  2. 脱敏边界 = 公司内跨部门：研发 → 技术支持 / 销售 一律脱敏；
  3. 原值仅「项目负责人 + 管理员」可见；
  4. 规则表由研发定稿（页面可增删 + 导出 JSON 交服务端）。

注入三处：CSS（最后一个 </style> 前）、区块 HTML（第二层 formErr 前）、
弹层 HTML（周会决议弹层前）、JS（window.__demoOK 前）。锚点各须唯一，否则报错。

用法：env -u PYTHONPATH /usr/bin/python3 脚本/build_rd_block.py [源.html 目标.html]
"""
import io
import os
import sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 基线（未注入研发板块）的 demo 源文件；切勿拿已注入的文件当源，否则会重复注入
SRC = os.path.join(ROOT, '工具', '客户拉通大表_demo_基线.html')
OUT = os.path.join(ROOT, '工具', '客户拉通大表_demo_v8_研发填写板块.html')
LIVE = os.path.join(ROOT, '工具', '客户拉通大表_demo.html')

CSS = r'''
/* ── 研发填写板块（研发交活口 · 出口自动脱敏）── */
.rd-note{font-size:11.5px;color:var(--sub);margin:0 0 10px;line-height:1.75;background:#f7faff;
  border:1px dashed #b9cbe8;border-radius:10px;padding:9px 13px}
.rd-note b{color:#7C3AED}
.rd-note code{background:#eef3fb;color:var(--pri-d);border-radius:4px;padding:0 4px;
  font-family:"SF Mono",Menlo,Consolas,monospace;font-size:11px}
.rd-ident{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 12px;background:#fbfdff;
  border:1px solid var(--line);border-radius:10px;padding:8px 12px}
.rd-ident-hd{font-size:11.5px;color:var(--sub);font-weight:700}
.rd-idb{border:1px solid var(--line);background:#fff;border-radius:8px;padding:4px 10px;font-size:12px;
  font-weight:700;color:#33465f;cursor:pointer;font-family:inherit}
.rd-idb:hover{border-color:var(--pri);color:var(--pri)}
.rd-idb.on{background:var(--pri);border-color:var(--pri);color:#fff}
.rd-ident-tip{font-size:11.5px;color:var(--mut);font-weight:700;margin-left:auto}
.rd-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
@media(max-width:900px){.rd-grid{grid-template-columns:1fr}}
.rd-col{border:1px solid var(--line);border-radius:12px;padding:11px 12px;background:#fbfdff;
  display:flex;flex-direction:column;gap:8px}
.rd-col.rawbox{background:#fff8f1;border-color:#f3d9b8}
.rd-hd{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12.5px;font-weight:800;color:var(--pri-d)}
.rd-lock{font-size:11px;font-weight:700;border-radius:7px;padding:1px 7px;color:#b25a00;
  background:#fff3e2;border:1px solid #f3d9b8}
.rd-lock.open{color:#0f7a3d;background:#e9f8ef;border-color:#a6ddba}
.rd-f{display:flex;flex-direction:column;gap:4px;font-size:11.5px;color:var(--sub);font-weight:600}
.rd-f input,.rd-f select{border:1px solid var(--line);border-radius:8px;padding:6px 8px;font-size:12.5px;
  background:#fff;color:var(--ink);font-family:inherit}
.rd-f input:focus,.rd-f select:focus{outline:none;border-color:var(--pri)}
.rd-ta{border:1px solid var(--line);border-radius:8px;padding:8px 9px;font-size:12.5px;min-height:104px;
  resize:vertical;background:#fff;color:var(--ink);font-family:inherit;line-height:1.65}
.rd-ta:focus{outline:none;border-color:var(--pri)}
.rd-ta:disabled{background:#eef1f5;color:var(--mut);cursor:not-allowed}
.rd-maskbox{border:1px solid #cfe9da;background:#f3fbf6;border-radius:8px;padding:8px 9px;font-size:12.5px;
  line-height:1.7;color:#0f3d24;word-break:break-word;white-space:pre-wrap;min-height:104px}
.rd-hint{font-size:11px;color:var(--mut);line-height:1.65}
.rd-sub{margin-top:12px;border:1px solid var(--line);border-radius:12px;padding:11px 12px;background:#fbfdff;
  display:flex;flex-direction:column;gap:7px}
.rd-sub input[type=file]{font-size:12px;font-family:inherit;color:var(--sub)}
.rd-preview{margin-top:12px;border:1px solid var(--line);border-radius:12px;overflow:hidden;background:#fff}
.rd-pv-hd{background:#eef3fb;color:var(--pri-d);font-size:12.5px;font-weight:800;padding:8px 12px;
  border-bottom:1px solid var(--line)}
.rd-pv-row{display:grid;grid-template-columns:76px minmax(0,1fr);gap:10px;padding:9px 12px;
  border-bottom:1px solid #f0f4fa;align-items:start}
.rd-pv-row.hide{display:none}
.rd-pv-row>b{font-size:11.5px;color:var(--sub)}
.rd-pv-box{font-size:12.5px;line-height:1.75;color:var(--ink);word-break:break-word;white-space:pre-wrap}
.rd-pv-box.out{background:#f3fbf6;border:1px solid #cfe9da;border-radius:8px;padding:6px 9px;color:#0f3d24}
.rd-pv-box mark{background:#ffe9a8;color:#7a4a12;border-radius:3px;padding:0 2px;font-weight:700}
.rd-pv-meta{padding:7px 12px;font-size:11.5px;font-weight:700;color:#0f7a3d;background:#f3fbf6}
.rd-pv-meta.zero{color:var(--mut);background:#fafbfd}
.rd-rules{margin-top:12px;border:1px solid var(--line);border-radius:12px;overflow:hidden;background:#fff}
.rd-rules-hd{background:#eef3fb;color:var(--pri-d);font-size:12.5px;font-weight:800;padding:8px 12px;
  border-bottom:1px solid var(--line);display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.rd-rules-hd .mini:first-of-type{margin-left:auto}
table.rd-rt{width:100%;border-collapse:collapse}
table.rd-rt th,table.rd-rt td{font-size:11.5px;padding:5px 9px;border-bottom:1px solid #f0f4fa;text-align:left;
  vertical-align:middle}
table.rd-rt th{background:#f7faff;color:var(--sub);font-weight:700;white-space:nowrap}
table.rd-rt th:last-child,table.rd-rt td:last-child{width:76px;white-space:nowrap;text-align:center}
table.rd-rt td .mini{white-space:nowrap}
table.rd-rt td input{width:100%;border:1px solid var(--line);border-radius:6px;padding:3px 6px;font-size:11.5px;
  font-family:inherit;background:#fff}
table.rd-rt td.builtin{color:var(--sub)}
table.rd-rt td.builtin b{color:var(--pri-d)}
.rd-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:12px}
.rd-role-tag{font-size:12px;font-weight:800;color:#fff;background:#7C3AED;border-radius:8px;padding:3px 11px}
.rd-role-tag.lock{background:#b25a00}
.btn[disabled]{opacity:.5;cursor:not-allowed}
.rd-audit{margin-top:12px;border:1px dashed #b9cbe8;border-radius:10px;padding:9px 13px;background:#f7faff}
.rd-audit-hd{font-size:12px;font-weight:800;color:var(--pri-d)}
.rd-audit ul{margin:6px 0 0;padding-left:18px;font-size:11.5px;color:var(--sub);line-height:1.85}
.rd-audit li.empty{color:var(--mut);list-style:none;margin-left:-18px}
'''

MARKUP = r'''      <div class="grp-title">研发填写板块（研发交活口 · 跨部门出口自动脱敏）</div>
      <div class="rd-note">
        研发在这里交活：<b>实验记录 + 结论 + 附件</b>。按口径 —— 流向<b>技术支持 / 销售</b>的内容一律走脱敏版
        （数值区间化 · 添加剂代号化 · 批号截尾），<b>原值只有「项目负责人」与「管理员」可见</b>，
        原值每被查看一次就写一条审计流水。脱敏在<b>服务端写入前</b>执行；规则表由<b>研发定稿</b>（下方可增删并导出 JSON 交服务端）。
      </div>

      <div class="rd-ident">
        <span class="rd-ident-hd">演示 · 切换视角看脱敏效果</span>
        <button class="rd-idb on" data-view="owner" type="button">研发项目负责人</button>
        <button class="rd-idb" data-view="tech" type="button">技术支持</button>
        <button class="rd-idb" data-view="sales" type="button">销售员</button>
        <button class="rd-idb" data-view="admin" type="button">管理员</button>
        <span class="rd-ident-tip" id="rdViewTip"></span>
      </div>

      <div class="rd-grid">
        <div class="rd-col">
          <div class="rd-hd">① 记录基本信息</div>
          <label class="rd-f">关联商机<select id="rd_opp"></select></label>
          <label class="rd-f">研发项目单号<input id="rd_no" value="RD-2026-1011-01"></label>
          <label class="rd-f">实验编号<input id="rd_exp" value="EXP-1011-03"></label>
          <label class="rd-f">体系版本<input id="rd_ver" value="V3.2"></label>
          <label class="rd-f">负责人<input id="rd_owner" value="研发 · 王工"></label>
        </div>
        <div class="rd-col">
          <div class="rd-hd">② 实验记录（条件 → 结果 → 判定）</div>
          <textarea class="rd-ta" id="rd_result">37℃ 加速 7 天：Ct 漂移 +0.4，判定 超阈</textarea>
          <div class="rd-hint">写量化结果与判定；这一段同样会过一遍脱敏（可能含原始数值）。</div>
        </div>
        <div class="rd-col rawbox">
          <div class="rd-hd">③ 敏感原值区 <span class="rd-lock" id="rdLockTag">🔒 原值可见</span></div>
          <textarea class="rd-ta" id="rd_raw">体系 10× 浓缩；酶加量 1.2×；保护剂 海藻糖 8%(w/v) + 甘露醇 3%(w/v)；批号 20260914</textarea>
          <div class="rd-maskbox" id="rdRawMask" style="display:none"></div>
          <div class="rd-hint">这里写真实名称与数值（组分用量、添加剂真实名称与用量、体系倍数、批号）。提交时按规则表生成脱敏版。</div>
        </div>
        <div class="rd-col">
          <div class="rd-hd">④ 结论（可对外口径）</div>
          <textarea class="rd-ta" id="rd_concl">体系稳定性未达标，需调整保护剂配比后复测</textarea>
          <div class="rd-hint">结论本身按可对外写；仍会过一遍脱敏以防夹带数值。</div>
        </div>
      </div>

      <div class="rd-sub">
        <div class="rd-hd">⑤ 附件（报告 / 图谱 / 原始数据）</div>
        <input type="file" id="rd_file" multiple>
        <div class="rd-hint" id="rdFileList">未选择文件 —— 演示不落盘，仅记录文件名与大小。上线后：上传即做「文本层脱敏 + 水印 + 下载留痕」，跨部门只下载到脱敏版。</div>
      </div>

      <div class="rd-preview">
        <div class="rd-pv-hd">脱敏预览（服务端规则的等价演示；生产环境原值不下发到非授权浏览器）</div>
        <div class="rd-pv-row" id="rdPvRawRow"><b>原值</b><div class="rd-pv-box" id="rdPvRaw">—</div></div>
        <div class="rd-pv-row"><b>跨部门可见</b><div class="rd-pv-box out" id="rdPvOut">—</div></div>
        <div class="rd-pv-meta" id="rdPvMeta">命中 0 处</div>
      </div>

      <div class="rd-rules">
        <div class="rd-rules-hd">脱敏规则表（研发定稿）
          <button class="mini" id="rdRuleAdd" type="button">＋ 加一条</button>
          <button class="mini" id="rdRuleExport" type="button">导出 JSON（交服务端）</button>
          <button class="mini" id="rdRuleReset" type="button">恢复默认</button>
        </div>
        <table class="rd-rt" id="rdRuleTable"></table>
        <div class="rd-hint" style="padding:7px 12px">
          带 <b>内置</b> 的三条（数值区间化 / 倍数档位化 / 批号截尾）由系统执行、不可删除；名称 → 代号 对照由研发维护。
          导出的是整张规则表（含版本号），服务端以它为准执行；前端改规则不影响服务端结果。
        </div>
      </div>

      <div class="rd-actions">
        <span class="rd-role-tag" id="rdRoleTag">研发项目负责人</span>
        <button class="btn" id="rdSubmit" type="button">提交研发记录 → 三轨落盘（跨部门用脱敏版）</button>
        <span class="tip">① 本地记录 ② CloudBase <code>rnd_records</code>（原值密文 + 脱敏值两列）③ 飞书描述（仅脱敏版 + 结论）。</span>
      </div>

      <div class="rd-audit">
        <div class="rd-audit-hd">原值查看审计流水（只追加 · 上线后写服务端）</div>
        <ul id="rdAuditList"></ul>
      </div>

'''

MODAL = r'''<!-- 研发记录 · 三轨落盘 弹层 -->
<div class="modal" id="rdModal">
  <div class="mh"><h3>研发记录 · 三轨落盘（跨部门出口 = 脱敏版）</h3><button class="x" id="rdX">✕</button></div>
  <div class="mb">
    <section>
      <h4>① 本地记录（研发记录号 <span id="rdRecId" style="color:#0d3a85"></span>）</h4>
      <pre id="rdLocal"></pre>
    </section>
    <section>
      <h4>② CloudBase 写入 JSON（集合 <code>rnd_records</code> · 原值密文 + 脱敏值两列）</h4>
      <pre id="rdCB"></pre>
    </section>
    <section>
      <h4>③ 飞书描述（一键复制 · 只有脱敏版）<button class="mini" id="rdCopy" type="button">复制</button></h4>
      <pre id="rdFeishu"></pre>
    </section>
  </div>
</div>

'''

JS = r'''
/* ================= 研发填写板块（研发交活口 · 跨部门出口自动脱敏） =================
   口径（2026-10-11 定）：脱敏边界 = 公司内跨部门（研发 → 技术支持 / 销售）；
   原值仅「项目负责人 + 管理员」可见；规则表由研发定稿，服务端以导出的 JSON 为准执行。 */
const RD_VIEWS = {owner:'研发项目负责人', tech:'技术支持', sales:'销售员', admin:'管理员'};
const RD_DICT_DEFAULT = [['海藻糖','添加剂A'],['甘露醇','添加剂B'],['甜菜碱','添加剂C'],['BSA','添加剂D'],
                         ['DMSO','添加剂E'],['甘油','添加剂F'],['吐温','添加剂G'],['聚乙二醇','添加剂H'],
                         ['蔗糖','添加剂I'],['牛血清白蛋白','添加剂D']];
const RD_BUILTIN = [
  ['数值区间化','带单位用量：%(w/v) / mM / μM / nM / mg/mL / μg / ng / U/μL / U/mL / U','±20% 区间 → 6.4–9.6%(w/v)、4.0–6.0mM'],
  ['倍数档位化','倍数 ≥5×（如 10×、25×）','档位 → （高倍浓缩体系）'],
  ['倍数区间化','倍数 <5×（如 酶加量 1.2×）','±20% 区间 → 0.96–1.44×'],
  ['批号截尾','批号 + 6 位以上数字','仅保留后 4 位 → 批号 ……0914']
];
let rdView = 'owner';
let rdRules = null;
let rdAudits = [];

function rdEscRe(s){ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
function rdLoad(key, dflt){
  try{ const v = localStorage.getItem('rdBlock::' + key); return v ? JSON.parse(v) : dflt; }catch(e){ return dflt; }
}
function rdSave(key, v){ try{ localStorage.setItem('rdBlock::' + key, JSON.stringify(v)); }catch(e){} }
function rdIsOwner(){ return rdView === 'owner' || rdView === 'admin'; }

/* —— 规则表：默认名称→代号 8 组 + 4 条内置 —— */
function rdDefaultRules(){
  return RD_DICT_DEFAULT.map(function(p){ return {type:'dict', from:p[0], to:p[1]}; });
}
function rdInitRules(){
  const saved = rdLoad('rules', null);
  rdRules = (saved && saved.length) ? saved : rdDefaultRules();
}

/* —— 脱敏引擎（等价演示；生产在服务端写入前执行）—— */
function rdDesens(text){
  let out = String(text == null ? '' : text);
  const hits = [];
  rdRules.filter(function(r){ return r.type === 'dict'; }).forEach(function(r){
    const from = (r.from || '').trim(), to = (r.to || '').trim();
    if(!from || !to) return;
    out = out.replace(new RegExp(rdEscRe(from), 'gi'), function(m){
      hits.push({t:'名称代号化', from:m, to:to}); return to;
    });
  });
  out = out.replace(/(\d+(?:\.\d+)?)\s*(%(?:\s*\((?:w\/v|v\/v|W\/V|V\/V)\))?|mM|μM|uM|nM|mg\/mL|μg\/mL|ug\/mL|ng\/mL|mg|μg|ug|ng|U\/μL|U\/uL|U\/mL|U)(?![\w%])/g,
    function(m, num, unit){
      const v = parseFloat(num);
      const f = function(x){ return x.toFixed(2).replace(/0+$/,'').replace(/\.$/,''); };
      const to = f(v*0.8) + '–' + f(v*1.2) + String(unit).replace(/\s+/g,'');
      hits.push({t:'数值区间化', from:m.trim(), to:to}); return to;
    });
  out = out.replace(/(\d+(?:\.\d+)?)\s*[×x](?:\s*浓缩)?(?![\w])/g, function(m, num){
    const v = parseFloat(num);
    if(v >= 5){
      const band = v >= 10 ? '高' : '中';
      const to = '（' + band + '倍浓缩体系）';
      hits.push({t:'倍数档位化', from:m.trim(), to:to}); return to;
    }
    const lo = (v*0.8).toFixed(2).replace(/0+$/,'').replace(/\.$/,'');
    const hi = (v*1.2).toFixed(2).replace(/0+$/,'').replace(/\.$/,'');
    const to = lo + '–' + hi + '×';
    hits.push({t:'倍数区间化', from:m.trim(), to:to}); return to;
  });
  out = out.replace(/批号[:：]?\s*([A-Za-z0-9]{6,})/g, function(m, lot){
    const to = '批号 ……' + lot.slice(-4);
    hits.push({t:'批号截尾', from:m.trim(), to:to}); return to;
  });
  return {out:out, hits:hits};
}

/* —— 规则表渲染 —— */
function rdRuleVersion(){ return '研发定稿 v1 · ' + rdRules.filter(function(r){return r.type==='dict';}).length + ' 组名称对照'; }
function rdRenderRules(){
  const t = $('rdRuleTable');
  let h = '<tr><th style="width:112px">类型</th><th>原文（命中即替换）</th><th>替换为</th><th style="width:56px">操作</th></tr>';
  t.innerHTML = h;
  rdRules.forEach(function(r, i){
    if(r.type !== 'dict') return;
    const tr = document.createElement('tr');
    tr.innerHTML = '<td>名称 → 代号</td>'
      + '<td><input class="rd-rf" data-i="' + i + '" data-k="from" value="' + esc(r.from || '') + '"></td>'
      + '<td><input class="rd-rf" data-i="' + i + '" data-k="to" value="' + esc(r.to || '') + '"></td>'
      + '<td><button class="mini rd-del" data-i="' + i + '" type="button">删除</button></td>';
    t.appendChild(tr);
  });
  RD_BUILTIN.forEach(function(b){
    const tr = document.createElement('tr');
    tr.innerHTML = '<td class="builtin"><b>内置</b></td><td class="builtin">' + esc(b[1]) + '</td>'
      + '<td class="builtin">' + esc(b[2]) + '</td><td class="builtin">不可删</td>';
    t.appendChild(tr);
  });
  t.querySelectorAll('.rd-rf').forEach(function(inp){
    inp.oninput = function(){
      const i = +inp.dataset.i;
      rdRules[i][inp.dataset.k] = inp.value;
      rdSave('rules', rdRules); rdPreview();
    };
  });
  t.querySelectorAll('.rd-del').forEach(function(b){
    b.onclick = function(){
      rdRules.splice(+b.dataset.i, 1); rdSave('rules', rdRules); rdRenderRules(); rdPreview();
      toast('已删除 1 条名称对照');
    };
  });
}

/* —— 脱敏预览 —— */
function rdHl(s, hits){
  let out = esc(s);
  hits.forEach(function(h){
    if(!h.to) return;
    const re = new RegExp(rdEscRe(esc(h.to)), 'g');
    out = out.replace(re, function(m, off){
      if(out.slice(Math.max(0, off - 4), off) === '<mark') return m;
      return '<mark>' + m + '</mark>';
    });
  });
  return out;
}
function rdPreview(){
  const raw = $('rd_raw').value, owner = rdIsOwner();
  const dRaw = rdDesens(raw), dRes = rdDesens($('rd_result').value), dCon = rdDesens($('rd_concl').value);
  const total = dRaw.hits.length + dRes.hits.length + dCon.hits.length;
  $('rdPvRaw').textContent = raw;
  $('rdPvRawRow').classList.toggle('hide', !owner);
  $('rdPvOut').innerHTML = rdHl(dRaw.out, dRaw.hits);
  $('rdPvMeta').textContent = '本次共命中 ' + total + ' 处（原值 ' + dRaw.hits.length
    + ' · 实验记录 ' + dRes.hits.length + ' · 结论 ' + dCon.hits.length + '）｜ 规则版本：' + rdRuleVersion();
  $('rdPvMeta').classList.toggle('zero', total === 0);
  if(!owner) $('rdRawMask').innerHTML = rdHl(dRaw.out, dRaw.hits);
  window.__rdPv = {hits:total, out:dRaw.out, rawVisible:owner,
                   types:dRaw.hits.map(function(h){ return h.t; })};
  return {dRaw:dRaw, dRes:dRes, dCon:dCon, total:total};
}

/* —— 视角切换（谁看原值 / 谁只看脱敏版）—— */
function rdAudit(msg){
  rdAudits.unshift({t:new Date().toLocaleTimeString('zh-CN', {hour12:false}), who:RD_VIEWS[rdView], msg:msg});
  rdAudits = rdAudits.slice(0, 12);
  $('rdAuditList').innerHTML = rdAudits.map(function(a){
    return '<li>' + esc(a.t) + ' ｜ ' + esc(a.who) + ' ｜ ' + esc(a.msg) + '</li>'; }).join('')
    || '<li class="empty">暂无记录</li>';
  window.__rdAudit = rdAudits.slice();
}
function rdApplyView(){
  const owner = rdIsOwner();
  document.querySelectorAll('.rd-idb').forEach(function(b){ b.classList.toggle('on', b.dataset.view === rdView); });
  $('rd_raw').disabled = !owner;
  $('rd_raw').style.display = owner ? '' : 'none';
  $('rdRawMask').style.display = owner ? 'none' : '';
  const tag = $('rdLockTag');
  tag.className = 'rd-lock' + (owner ? ' open' : '');
  tag.textContent = owner ? (rdView === 'admin' ? '🔓 管理员 · 原值可见' : '🔓 项目负责人 · 原值可见')
                          : '🔒 原值不可见（跨部门只给脱敏版）';
  $('rdRoleTag').textContent = RD_VIEWS[rdView];
  $('rdRoleTag').className = 'rd-role-tag' + (owner ? '' : ' lock');
  $('rdSubmit').disabled = !owner;
  $('rdSubmit').title = owner ? '' : '仅研发项目负责人 / 管理员可提交研发记录';
  $('rdViewTip').textContent = owner ? '原值可编辑 · 提交时自动生成跨部门脱敏版'
                                     : '跨部门视角：原值不下发，只显示脱敏版';
  const res = rdPreview();
  rdAudit(owner ? ('查看原值（命中 ' + res.total + ' 处待脱敏）') : '尝试查看原值 → 已拦截（无权限）');
}

/* —— 附件（演示：只记录文件名/大小）—— */
function rdFileInfo(){
  const f = $('rd_file').files || [];
  if(!f.length){ $('rdFileList').textContent = '未选择文件 —— 演示不落盘，仅记录文件名与大小。'; return []; }
  const list = [];
  for(let i = 0; i < f.length; i++){ list.push({name:f[i].name, size:f[i].size}); }
  $('rdFileList').textContent = '已选 ' + list.length + ' 个：' + list.map(function(x){
    return x.name + '（' + (x.size/1024).toFixed(1) + ' KB）'; }).join('、')
    + '　—— 上线后上传即做文本层脱敏 + 水印，跨部门只能下载脱敏版。';
  return list;
}

/* —— 提交：三轨落盘（跨部门出口 = 脱敏版）—— */
function rdSubmit(){
  if(!rdIsOwner()){ toast('仅研发项目负责人 / 管理员可提交'); return; }
  const pv = rdPreview();
  const recId = 'RD-' + ($('f_date').value || '2026-10-11').replace(/-/g,'') + '-' + pad2(activities.length + 1);
  const files = rdFileInfo();
  const base = {
    rd_id: recId, opportunity: $('rd_opp').value, rnd_no: $('rd_no').value.trim(),
    exp_no: $('rd_exp').value.trim(), version: $('rd_ver').value.trim(), owner: $('rd_owner').value.trim(),
    attachments: files.map(function(x){ return x.name; }), created_at: '2026-10-11T00:00:00+08:00',
    desens: {rules_version: rdRuleVersion(), hits: pv.total,
             types: pv.dRaw.hits.concat(pv.dRes.hits, pv.dCon.hits).map(function(h){ return h.t; })}
  };
  const localDoc = Object.assign({}, base, {
    result: $('rd_result').value.trim(), concl: $('rd_concl').value.trim(),
    masked: {result: pv.dRes.out, raw: pv.dRaw.out, concl: pv.dCon.out},
    raw_cipher: '[原值·AES 密文，仅项目负责人 / 管理员可解密]'
  });
  const cbDoc = {
    collection: 'rnd_records',
    doc: Object.assign({}, base, {
      result_masked: pv.dRes.out, concl: pv.dCon.out, raw_masked: pv.dRaw.out,
      raw_cipher: '[原值·AES 密文]',                     /* 原值绝不进共享字段 */
      raw_scope: ['project_owner', 'admin'],
      xdept_payload: {result: pv.dRes.out, concl: pv.dCon.out, raw: pv.dRaw.out}
    })
  };
  const feishu = '【研发记录 · 跨部门知会】' + recId + '\n'
    + '商机：' + base.opportunity + ' ｜ 研发项目单：' + base.rnd_no + ' ｜ 实验编号：' + base.exp_no
    + ' ｜ 体系版本：' + base.version + ' ｜ 负责人：' + base.owner + '\n'
    + '—— 实验记录（脱敏版）——\n' + pv.dRes.out + '\n'
    + '—— 敏感原值区（跨部门出口 · 脱敏版）——\n' + pv.dRaw.out + '\n'
    + '—— 结论 ——\n' + pv.dCon.out + '\n'
    + '—— 附件 ——\n' + (files.length ? files.map(function(x){ return x.name; }).join('、') : '（无）') + '\n'
    + '（本描述已按研发定稿规则表脱敏：命中 ' + pv.total + ' 处；原值仅项目负责人 / 管理员可查看）';
  $('rdRecId').textContent = recId;
  $('rdLocal').textContent = JSON.stringify(localDoc, null, 2);
  $('rdCB').textContent = JSON.stringify(cbDoc, null, 2);
  $('rdFeishu').textContent = feishu;
  $('rdModal').classList.add('open'); $('mask').classList.add('open');
  window.__rdLastSubmit = {recId:recId, hits:pv.total, feishu:feishu, cb:cbDoc, local:localDoc,
                           masked:cbDoc.doc.xdept_payload};
  rdAudit('提交研发记录 ' + recId + '（跨部门出脱敏版，命中 ' + pv.total + ' 处）');
  toast('研发记录已三轨落盘（跨部门用脱敏版）');
}

/* —— 规则表操作 —— */
function rdExportRules(){
  const payload = {rules_version: rdRuleVersion(), exported_at:'2026-10-11', owner:'研发部（定稿）',
    rules: {dict: rdRules.filter(function(r){ return r.type === 'dict'; }),
            builtin: RD_BUILTIN.map(function(b){ return {type:b[0], desc:b[1], effect:b[2], editable:false}; })}};
  const json = JSON.stringify(payload, null, 2);
  window.__rdRulesJson = json;
  try{
    const a = document.createElement('a');
    a.href = URL.createObjectURL(new Blob([json], {type:'application/json'}));
    a.download = '脱敏规则表_研发定稿.json';
    document.body.appendChild(a); a.click(); document.body.removeChild(a);
    setTimeout(function(){ URL.revokeObjectURL(a.href); }, 2000);
    toast('规则表已导出（脱敏规则表_研发定稿.json）');
  }catch(e){ toast('已生成规则表 JSON（见 __rdRulesJson）'); }
  return json;
}

function rdInit(){
  rdInitRules();
  const sel = $('rd_opp');
  const opps = [];
  try{
    L1.depts.forEach(function(d){ (d.rows || []).forEach(function(r){ if(r.opp && opps.indexOf(r.opp) < 0) opps.push(r.opp); }); });
  }catch(e){}
  (opps.length ? opps : ['（示例）O 客户 · 冻干体系放大']).forEach(function(o){ sel.appendChild(new Option(o, o)); });
  document.querySelectorAll('.rd-idb').forEach(function(b){
    b.onclick = function(){ rdView = b.dataset.view; rdApplyView(); };
  });
  $('rd_raw').oninput = rdPreview;
  $('rd_result').oninput = rdPreview;
  $('rd_concl').oninput = rdPreview;
  $('rd_file').onchange = rdFileInfo;
  $('rdSubmit').onclick = rdSubmit;
  $('rdRuleAdd').onclick = function(){
    rdRules.push({type:'dict', from:'', to:''}); rdSave('rules', rdRules); rdRenderRules(); rdPreview();
    toast('已加一条：填「原文」与「替换为」即生效');
  };
  $('rdRuleExport').onclick = rdExportRules;
  $('rdRuleReset').onclick = function(){
    rdRules = rdDefaultRules(); rdSave('rules', rdRules); rdRenderRules(); rdPreview(); toast('已恢复默认规则表');
  };
  $('rdX').onclick = function(){ $('rdModal').classList.remove('open'); $('mask').classList.remove('open'); };
  $('rdCopy').onclick = function(){
    const t = $('rdFeishu').textContent;
    if(navigator.clipboard && navigator.clipboard.writeText){
      navigator.clipboard.writeText(t).then(function(){ toast('飞书描述（脱敏版）已复制'); },
        function(){ fallbackCopy(t, function(){ toast('飞书描述（脱敏版）已复制'); }); });
    } else fallbackCopy(t, function(){ toast('飞书描述（脱敏版）已复制'); });
  };
  $('mask').addEventListener('click', function(){ $('rdModal').classList.remove('open'); });
  rdRenderRules();
  rdApplyView();
}
rdInit();
'''

MARK_ERR = '<div class="form-err" id="formErr"></div>'
MARK_MODAL = '<!-- 周会决议 · 落盘待办库 弹层 -->'
MARK_JS = 'window.__demoOK = {'


def inject(html):
    assert html.count('</style>') >= 1, '未找到 </style>'
    i = html.rfind('</style>')
    html = html[:i] + CSS + html[i:]

    for mark, block, label in ((MARK_ERR, MARKUP, '区块'),
                               (MARK_MODAL, MODAL, '弹层'),
                               (MARK_JS, JS, 'JS')):
        assert html.count(mark) == 1, '锚点不唯一：%s（%d 次）' % (label, html.count(mark))
        j = html.index(mark)
        html = html[:j] + block + html[j:]
    return html


def main():
    args = [a for a in sys.argv[1:] if not a.startswith('--')]
    promote = '--promote' in sys.argv
    src = args[0] if args else SRC
    out = args[1] if len(args) > 1 else OUT
    html = io.open(src, encoding='utf-8').read()
    if 'rd-ident' in html or 'rdBlock::' in html:
        raise SystemExit('❌ 源文件已含研发填写板块（%s）—— 请改用基线文件 %s，否则会重复注入。'
                         % (os.path.basename(src), os.path.basename(SRC)))
    new = inject(html)
    io.open(out, 'w', encoding='utf-8').write(new)
    print('源文件 : %s（%.1f KB）' % (os.path.basename(src), len(html.encode()) / 1024.0))
    print('产物   : %s（%.1f KB，+%.1f KB）' % (os.path.basename(out), len(new.encode()) / 1024.0,
                                               (len(new.encode()) - len(html.encode())) / 1024.0))
    print('守卫   : rdBlock 标记 %d ｜ rdModal %d ｜ rdSubmit 绑定 %d ｜ 零外链 %s'
          % (new.count('rd-ident'), new.count('id="rdModal"'), new.count('rdSubmit'),
             'OK' if ('http://' not in new.replace('http://www.w3.org', '')) else '发现外链'))
    if promote:
        io.open(LIVE, 'w', encoding='utf-8').write(new)
        print('已提升 : %s（现用 demo，等同 v8）' % os.path.basename(LIVE))
    print('锚点   : 各命中 1 次 ✅')


if __name__ == '__main__':
    main()
