#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""行业情报 · 全量 HTML 报告渲染器（自包含、零外链）。

输入：runs/<date>_highvalue.json（S/A/B 结构化）+ runs/<date>_items_first.jsonl（年度统计）
输出：经营分析/宝锐行业情报报告_<date>.html
能力：搜索 + 点击列头排序 + 不截断文本 + 品牌页眉页脚（脚本/brand_kit.py）
"""
import json
import os
import re
import sys
from collections import Counter
from datetime import datetime, timedelta

W = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
sys.path.insert(0, os.path.join(W, '脚本'))
import brand_kit as BK  # noqa: E402

RUNS = os.path.join(W, '数据/market_intel/runs')
DAY = sys.argv[1] if len(sys.argv) > 1 and sys.argv[1].isdigit() else datetime.now().strftime('%Y%m%d')
HV = json.load(open(os.path.join(RUNS, DAY + '_highvalue.json'), encoding='utf-8'))
FULL = [json.loads(l) for l in open(os.path.join(RUNS, DAY + '_items_first.jsonl'), encoding='utf-8') if l.strip()]


def yr(i):
    m = re.search(r'(20\d{2})', i.get('日期', '') or '')
    return m.group(1) if m else '日期缺'


cust = [i for i in FULL if i['来源'] == '巨潮公告']
comp = [i for i in FULL if i['来源'].startswith('竞品官网')]
bid = [i for i in FULL if i['来源'].startswith(('全国公共资源', '检索源'))]
YEARS = ['2026', '2025', '2024', '2023', '日期缺']
STAT = {'采集': len(FULL), '客户公告': len(cust), '竞品动态': len(comp), '招投标': len(bid),
        '提炼': len(HV['S']) + len(HV['A']) + len(HV['B']), 'S': len(HV['S']), 'A': len(HV['A']), 'B': len(HV['B'])}
REG = {y: sum(1 for i in cust if yr(i) == y and '注册证' in i['标题']) for y in YEARS[:4]}
BRANDS = list(dict.fromkeys(i['主体'] for i in comp))

CSS = """
*{box-sizing:border-box}
body{margin:0;background:#f4f6fa;color:#1b2333;font:14px/1.6 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif}
.wrap{max-width:1280px;margin:0 auto;padding:0 18px 56px}
h1{font-size:26px;margin:22px 0 6px}
h2{font-size:19px;margin:34px 0 10px;padding-left:10px;border-left:4px solid #1852B6}
h3{font-size:16px;margin:20px 0 8px}
.sub{color:#5b6478;font-size:13px;margin-bottom:10px}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin:16px 0}
.card{background:#fff;border-radius:12px;padding:14px 16px;box-shadow:0 1px 3px rgba(16,30,54,.08);border-top:3px solid #1852B6}
.card .n{font-size:24px;font-weight:700;color:#1852B6}
.card .l{font-size:12px;color:#5b6478;margin-top:2px}
.card.s{border-top-color:#c0392b}.card.a{border-top-color:#e08b1a}.card.b{border-top-color:#4a7ab5}
table{border-collapse:collapse;width:100%;background:#fff;border-radius:10px;overflow:hidden;box-shadow:0 1px 3px rgba(16,30,54,.08);font-size:13px}
th,td{border-bottom:1px solid #e6ebf3;padding:8px 10px;vertical-align:top;word-break:break-word;white-space:normal}
th{background:#eef3fb;color:#26324a;text-align:left;font-weight:600;cursor:pointer;user-select:none;position:sticky;top:0;z-index:2}
th:after{content:' ⇅';color:#9aa7bd;font-size:11px}
tr:hover td{background:#f7faff}
.tag{display:inline-block;padding:1px 7px;border-radius:9px;font-size:11px;margin:0 3px 2px 0}
.t-sku{background:#e8f3ff;color:#1852B6}.t-why{background:#fff3e0;color:#a25b00}.t-own{background:#e9f7ef;color:#1e7a44}
.tier{display:inline-block;width:22px;height:22px;line-height:22px;text-align:center;border-radius:6px;color:#fff;font-weight:700;font-size:12px}
.tier.S{background:#c0392b}.tier.A{background:#e08b1a}.tier.B{background:#4a7ab5}
.scard{background:#fff;border-radius:12px;padding:16px 18px;margin:14px 0;box-shadow:0 1px 3px rgba(16,30,54,.08);border-left:4px solid #c0392b}
.scard h3{margin:0 0 8px;font-size:17px}
.kv{display:grid;grid-template-columns:88px 1fr;gap:4px 12px;font-size:13px}
.kv .k{color:#5b6478}
.tools{display:flex;gap:10px;align-items:center;margin:10px 0;flex-wrap:wrap}
.tools input{padding:7px 11px;border:1px solid #cdd7e6;border-radius:8px;min-width:220px;font-size:13px}
.tools .cnt{color:#5b6478;font-size:12px}
.scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
a{color:#1852B6;text-decoration:none;word-break:break-all}
a:hover{text-decoration:underline}
.note{background:#fffbe6;border:1px solid #ffe58f;border-radius:10px;padding:12px 14px;font-size:13px;margin:14px 0}
@media(max-width:640px){.kv{grid-template-columns:1fr}.wrap{padding:0 12px 40px}h1{font-size:21px}}
/* 品牌页眉在窄屏不得溢出（brand_kit 注入的 .bior-header-inner 默认宽度会多出 14px） */
html,body{max-width:100%;overflow-x:hidden}
.bior-header,.bior-header-inner,.bior-footer{max-width:100%!important;box-sizing:border-box!important}
.bior-header-inner{padding-left:12px!important;padding-right:12px!important;flex-wrap:wrap!important}
.bior-footer,.bior-footer-inner{padding-left:12px!important;padding-right:12px!important}
"""

JS = """
function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
function renderTable(sel, rows, cols, opt){
  opt=opt||{};var host=document.querySelector(sel);if(!host)return;
  host.innerHTML='<div class="tools"><input placeholder="'+(opt.ph||'搜索…')+'" oninput="__f\\''+sel+'\\'"><span class="cnt" id="cnt'+sel+'"></span></div><div class="scroll"><table><thead></thead><tbody></tbody></table></div>';
  var state={q:'',key:null,dir:1};
  function draw(){
    var data=rows.filter(function(r){if(!state.q)return true;var s=state.q.toLowerCase();
      return Object.keys(r).some(function(k){return String(r[k]||'').toLowerCase().indexOf(s)>=0;});});
    if(state.key){data=data.slice().sort(function(a,b){var x=a[state.key],y=b[state.key];
      if(typeof x==='number'&&typeof y==='number')return (x-y)*state.dir;
      return String(x||'').localeCompare(String(y||''))*state.dir;});}
    var th=cols.map(function(c){return '<th onclick="__s\\''+sel+'\\',\\''+c.key+'\\'">'+esc(c.label)+'</th>';}).join('');
    var tb=data.map(function(r){return '<tr>'+cols.map(function(c){return '<td>'+(c.fmt?c.fmt(r):esc(r[c.key]))+'</td>';}).join('')+'</tr>';}).join('');
    host.querySelector('thead').innerHTML='<tr>'+th+'</tr>';
    host.querySelector('tbody').innerHTML=tb||'<tr><td colspan="'+cols.length+'">无匹配数据</td></tr>';
    var el=document.getElementById('cnt'+sel);if(el)el.textContent=data.length+' / '+rows.length+' 条';
  }
  window.__f=window.__f||{};window.__f[sel]=function(e){state.q=e.value;draw();};
  window.__s=window.__s||{};window.__s[sel]=function(k){if(state.key===k)state.dir=-state.dir;else{state.key=k;state.dir=1;}draw();};
  window.__f[sel].bind&&0;
  typeof __hookInput==='function'&&__hookInput(sel);
  draw();
}
function bindSearch(){
  document.querySelectorAll('.tools input').forEach(function(inp){
    if(inp.__b)return;inp.__b=1;
    inp.addEventListener('input',function(){var f=window.__f[inp.dataset.sel];f&&f(inp);});
  });
}
"""


def cell_skus(it):
    if not it['货号']:
        return '<span class="tag t-sku">未匹配（需人工选型）</span>'
    return ''.join('<span class="tag t-sku">%s %s</span>' % (esc(x['货号']), esc(x['品名'][:28])) for x in it['货号'])


def esc(s):
    return (str(s).replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;'))


H = []
H.append('<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">')
H.append('<meta name="viewport" content="width=device-width,initial-scale=1">')
H.append('<title>宝锐生物 · 行业情报全量报告 %s</title>' % DAY)
H.append('<style>%s</style></head><body><div class="wrap">' % CSS)
H.append('<h1>行业情报 · 全量调研报告</h1>')
H.append('<div class="sub">生成 %s ｜ 时间窗：近三年（1095 天）｜ 覆盖：10 家上市客户体系 + 6 家竞品官网 + 招投标检索源（海尔施 601206 经实测排除：IPO 暂缓后无披露）</div>' % datetime.now().strftime('%Y-%m-%d %H:%M'))
H.append('<div class="cards">')
for k, cls in [('采集', ''), ('客户公告', ''), ('竞品动态', ''), ('招投标', ''), ('提炼', ''), ('S', 's'), ('A', 'a'), ('B', 'b')]:
    H.append('<div class="card %s"><div class="n">%s</div><div class="l">%s</div></div>' % (cls, STAT[k], k + (' 档' if k in 'SAB' and len(k) == 1 else '')))
H.append('</div>')

H.append('<div class="note"><b>判定口径</b>：客户信号优先 —— 拿证 +8 ｜ 中标/合同 +7 ｜ 经营风险（亏损/减值/集采）+6 ｜ 产品获批 +5 ｜ 资本合作 +3 ｜ 与我方有关联 +3；'
         '竞品侧降权 —— 命中我方赛道 +3 ｜ 新品 +2。时效：≤30 天 +2、≤90 天 +1，<b>超 90 天不得进 S 档</b>。'
         '分档：<span class="tier S">S</span> ≥13 ｜ <span class="tier A">A</span> 9–12 ｜ <span class="tier B">B</span> 5–8。'
         '货号建议由 <b>curated 分桶规则</b> 从 84 个真实货号中取（非模型生成）；主责人由客户归属表映射。</div>')

H.append('<h2>一、S 档 · 需本周动作（%d 条）</h2>' % len(HV['S']))
for n, it in enumerate(HV['S'], 1):
    p = it['要点']
    H.append('<div class="scard"><h3><span class="tier S">S%d</span> [%d 分] %s</h3>' % (n, it['分数'], esc(it['标题'][:120])))
    H.append('<div class="kv">')
    for k in ('时间', '地点', '人物', '主体'):
        H.append('<div class="k">%s</div><div>%s</div>' % (k, esc(p.get(k, '未提及'))))
    H.append('<div class="k">事件</div><div><b>%s</b></div>' % esc(p.get('事件', '未提及')))
    H.append('<div class="k">关键事项</div><div>%s</div>' % esc(p.get('关键事项', '未提及')))
    H.append('<div class="k">与我方关联</div><div>%s</div>' % esc(p.get('与我方关联', '未提及')))
    H.append('<div class="k">可推货号</div><div>%s</div>' % cell_skus(it))
    H.append('<div class="k">评分理由</div><div>%s</div>' % esc('；'.join(it['理由'])))
    H.append('<div class="k">建议动作</div><div><b>%s</b></div>' % esc(it['建议动作']))
    H.append('<div class="k">主责</div><div><span class="tag t-own">%s</span></div>' % esc(it['主责']))
    H.append('<div class="k">来源</div><div>%s ｜ %s ｜ <a href="%s" target="_blank">%s</a></div>' % (esc(it['来源']), esc(it['日期'] or '—'), esc(it['链接']), esc(it['链接'][:70])))
    H.append('</div></div>')

H.append('<h2>二、A 档 · 30 天内跟进（%d 条）</h2>' % len(HV['A']))
H.append('<div id="aT"></div>')

H.append('<h2>三、B 档 · 存量索引（%d 条）</h2>' % len(HV['B']))
H.append('<div id="bT"></div>')

H.append('<h2>四、三年趋势（全量口径，确定性统计）</h2>')
H.append('<div class="scroll"><table><thead><tr><th>年份</th><th>客户公告</th><th>竞品动态</th><th>招投标</th><th>合计</th><th>其中：客户注册证</th></tr></thead><tbody>')
for y in YEARS:
    H.append('<tr><td>%s</td><td>%d</td><td>%d</td><td>%d</td><td>%d</td><td>%s</td></tr>' % (
        y, sum(1 for i in cust if yr(i) == y), sum(1 for i in comp if yr(i) == y),
        sum(1 for i in bid if yr(i) == y), sum(1 for i in FULL if yr(i) == y),
        REG.get(y, '—')))
H.append('</tbody></table></div>')
H.append('<h3>竞品发布节奏（年 × 品牌）</h3><div class="scroll"><table><thead><tr><th>品牌</th>%s</tr></thead><tbody>' %
         ''.join('<th>%s</th>' % y for y in YEARS))
for b in BRANDS:
    H.append('<tr><td>%s</td>%s</tr>' % (esc(b), ''.join('<td>%d</td>' % sum(1 for i in comp if i['主体'] == b and yr(i) == y) for y in YEARS)))
H.append('</tbody></table></div>')

H.append('<h2>五、数据来源与已知缺口</h2>')
H.append('<div class="scroll"><table><thead><tr><th>源</th><th>状态</th><th>说明</th></tr></thead><tbody>')
for src, st, note in [
    ('巨潮公告（10 主体）', '🟢 正常', 'POST 接口；按时间窗翻页，客户注册证/中标/合作信号'),
    ('海尔施集团（601206）', '⚪ 排除', '实测：巨潮仅 6 条公告且全为 2016-08 IPO 文件（最新为「A股暂缓发行」）→ IPO 暂缓后无披露，非有效上市监测标的'),
    ('全式金 / 赛默飞 / 翌圣 / 诺唯赞 / 罗氏 / 菲鹏', '🟢 正常', '官网新闻列表页；全式金服务端分页 12 页'),
    ('翌圣 / 诺唯赞 三年回溯', '🟡 受限', '翌圣 robots 禁 query 参数；诺唯赞列表前端渲染 → v0.2 补'),
    ('罗氏诊断', '🟠 存档少', 'sitemap 仅存 14 条新闻，三年窗口内条目偏少'),
    ('ggzy 全国公共资源交易平台', '🟡 待接入', '列表 JS 渲染 + getTradList 404 + 搜索 POST 405 → v0.2'),
    ('招投标（检索源兜底）', '🟢 正常', 'web_search 二级来源，已标注'),
    ('迈迪安', '⚪ 跳过', 'Cloudflare 403'),
    ('宝生物', '🟠 空', '官网新闻路径未定位；2026-06-12 已退市（无财报）'),
    ('NMPA 注册证', '🔴 不可用', '主站 412 风控 → 改用上市客户巨潮公告替代'),
]:
    H.append('<tr><td>%s</td><td>%s</td><td>%s</td></tr>' % (src, st, note))
H.append('</tbody></table></div>')

# 表格数据 + 脚本
A_JS = json.dumps([{'tier': 'A', '分数': x['分数'], '日期': x['日期'], '主体': x['主体'], '事件': str(x['要点'].get('事件', '')),
                    '关联': str(x['要点'].get('与我方关联', '')), '货号': '；'.join('%s %s' % (s['货号'], s['品名'][:20]) for s in x['货号']),
                    '动作': x['建议动作'], '主责': x['主责'], '链接': x['链接'], '标题': x['标题']} for x in HV['A']], ensure_ascii=False)
B_JS = json.dumps([{'分数': x['分数'], '日期': x['日期'], '主体': x['主体'], '标题': x['标题'],
                    '理由': '；'.join(x['理由']), '链接': x['链接']} for x in HV['B']], ensure_ascii=False)

H.append('''<script>%s
var A=%s, B=%s;
function L(u){return '<a href="'+u+'" target="_blank">'+u.slice(0,58)+'</a>';}
renderTable('#aT', A, [
 {key:'日期',label:'日期'},{key:'主体',label:'主体'},{key:'事件',label:'事件'},{key:'关联',label:'与我方关联'},
 {key:'货号',label:'可推货号'},{key:'动作',label:'建议动作'},{key:'主责',label:'主责'},
 {key:'链接',label:'原文',fmt:function(r){return L(r.链接);}}], {ph:'搜 A 档：主体/事件/货号/主责…'});
renderTable('#bT', B, [
 {key:'分数',label:'分',type:'number'},{key:'日期',label:'日期'},{key:'主体',label:'主体'},{key:'标题',label:'标题'},
 {key:'理由',label:'命中理由'},{key:'链接',label:'原文',fmt:function(r){return L(r.链接);}}], {ph:'搜 B 档：关键词/主体/理由…'});
</script>''' % (JS, A_JS, B_JS))

H.append('</div><script>')
for sel in ('#aT', '#bT'):
    H.append("var i=document.querySelectorAll('.tools input');")
    break
H.append('''document.querySelectorAll('.tools input').forEach(function(inp){
  if(inp.__b)return; inp.__b=1;
  var box=inp.closest('#aT')?'#aT':'#bT';
  inp.dataset.sel=box;
  inp.addEventListener('input',function(){var f=window.__f[box]; f&&f(inp);});
});
</script></body></html>''')

html = '\n'.join(H)
html = BK.inject(html, theme='dark', tag='行业情报 · 全量调研报告')
out = os.path.join(W, '经营分析', '宝锐行业情报报告_%s.html' % DAY)
os.makedirs(os.path.dirname(out), exist_ok=True)
open(out, 'w', encoding='utf-8').write(html)
print('✅ HTML 报告已生成:', out)
print('   大小 %.1f KB ｜ S %d ／ A %d ／ B %d ｜ 表格 %d 个' % (
    len(html.encode()) / 1024, len(HV['S']), len(HV['A']), len(HV['B']), html.count('<table')))
print('   品牌校验:', BK.verify(html) if hasattr(BK, 'verify') else '—')
