// brain-desk.jsx — 研判中樞 / The Desk（量化平台 Phase 3）
// 上終端下紙張的混血版面：
//   ① qd-term hero：TAIEX mega 金字(useQuote 訂閱) + 研判大印 + 信心儀表 + 主席引文
//   ② 訊號燈牆：34 檔 qd-tile（composite 四檔燈），點開五力雷達詳情
//   ③ 命中率追蹤：策略勝率表 + 每日訊號熱力日曆（D1 冷啟動空狀態必做）
//   ④ 紙張收尾：方法論短文 + 非投資建議
// 資料：type=brain / type=signal_stats 各抓一次；TAIEX 即時走 QuoteHub 訂閱（禁自開輪詢）
const { useState, useEffect, useRef, useMemo } = React;

function fmtSign(v, unit) {
  if (v == null) return '—';
  return (v > 0 ? '+' : '') + v + (unit || '');
}

const STANCE_STYLE = {
  '進攻': { color: '#e0564f', label: 'Offense' },
  '中性': { color: 'var(--term-gold)', label: 'Neutral' },
  '防守': { color: '#3fa17a', label: 'Defense' },
};

// regime 色（qd-term 紅漲綠跌慣例）：多頭紅／空頭綠／盤整金
const REGIME_STYLE = {
  '多頭': { color: 'var(--term-up)' },
  '空頭': { color: 'var(--term-dn)' },
  '盤整': { color: 'var(--term-flat)' },
};

// ── 信心儀表（echarts gauge，terminal 主題）──
function ConfidenceGauge({ value, stance }) {
  const ref = useRef(null);
  useEffect(() => {
    if (!ref.current || typeof echarts === 'undefined' || value == null) return;
    const inst = echarts.init(ref.current, 'formosa-term', { renderer: 'svg' });
    const col = (STANCE_STYLE[stance] || {}).color || '#d4b370';
    inst.setOption({
      series: [{
        type: 'gauge', startAngle: 200, endAngle: -20, min: 0, max: 100,
        radius: '95%', center: ['50%', '62%'],
        progress: { show: true, width: 9, itemStyle: { color: col } },
        axisLine: { lineStyle: { width: 9, color: [[1, 'rgba(139,145,165,.22)']] } },
        axisTick: { show: false }, splitLine: { show: false }, axisLabel: { show: false },
        pointer: { show: false }, anchor: { show: false },
        detail: {
          valueAnimation: true, offsetCenter: [0, '-12%'],
          formatter: '{value}%', color: col,
          fontSize: 26, fontWeight: 700, fontFamily: "'Times New Roman',serif",
        },
        data: [{ value }],
      }],
    });
    const ro = new ResizeObserver(() => { if (!inst.isDisposed()) inst.resize(); });
    ro.observe(ref.current);
    return () => { ro.disconnect(); inst.dispose(); };
  }, [value, stance]);
  return <div ref={ref} style={{ width: 130, height: 100 }} />;
}

// ── 五力雷達圖（tile 點開詳情用）──
function FiveRadar({ five }) {
  const ref = useRef(null);
  useEffect(() => {
    if (!ref.current || typeof echarts === 'undefined' || !five) return;
    const inst = echarts.init(ref.current, 'formosa-term', { renderer: 'svg' });
    inst.setOption({
      radar: {
        indicator: [
          { name: '趨勢', max: 25 }, { name: '動能', max: 20 },
          { name: '籌碼', max: 15 }, { name: '財務', max: 20 }, { name: '估值', max: 20 },
        ],
        radius: '62%',
        axisName: { fontFamily: "'BiauKai','標楷體',serif", fontSize: 11.5, color: '#8b91a5' },
        splitLine: { lineStyle: { color: 'rgba(139,145,165,.2)' } },
        splitArea: { show: false },
        axisLine: { lineStyle: { color: 'rgba(139,145,165,.2)' } },
      },
      series: [{
        type: 'radar',
        data: [{ value: [five.pTrend, five.pMom, five.pChip || 6, five.pQual || 8, five.pVal || 8] }],
        areaStyle: { color: 'rgba(212,179,112,.25)' },
        lineStyle: { color: '#d4b370', width: 1.5 },
        symbolSize: 3, itemStyle: { color: '#d4b370' },
      }],
    });
    return () => inst.dispose();
  }, [five]);
  return <div ref={ref} style={{ width: '100%', height: 170 }} />;
}

// ── 每日訊號密度長條圖（紙張區）──
//   前身是 echarts 月曆熱力圖(cellSize 'auto')，冷啟動只有數天資料時
//   週欄會被拉成滿版橫條而看似壞掉。改長條圖：3 天畫 3 根、90 天畫 90 根都合理。
function SignalDensityChart({ byDay }) {
  const ref = useRef(null);
  useEffect(() => {
    if (!ref.current || typeof echarts === 'undefined' || !byDay || !byDay.length) return;
    const inst = echarts.init(ref.current, 'formosa', { renderer: 'svg' });
    const recent = byDay.slice(-30);                 // 近 30 個掃描日
    const many = recent.length > 12;
    inst.setOption({
      grid: { left: 32, right: 12, top: 14, bottom: 24 },
      tooltip: {
        trigger: 'axis',
        formatter: p => `${p[0].axisValue}<br/>訊號 <b>${p[0].data}</b> 個`,
      },
      xAxis: {
        type: 'category',
        data: recent.map(d => (d.scan_date || '').slice(5)),   // MM-DD
        axisLabel: { fontSize: 11.5, color: '#6a6f80', fontFamily: "'Times New Roman',serif",
          interval: many ? Math.floor(recent.length / 8) : 0 },
        axisTick: { show: false },
        axisLine: { lineStyle: { color: '#d8cdb0' } },
      },
      yAxis: {
        type: 'value', minInterval: 1,
        axisLabel: { fontSize: 11.5, color: '#9a9789', fontFamily: "'Times New Roman',serif" },
        splitLine: { lineStyle: { color: '#eae4d6' } },
      },
      series: [{
        type: 'bar', data: recent.map(d => d.signals),
        barMaxWidth: 28,
        itemStyle: { color: '#b8924d' },
        emphasis: { itemStyle: { color: '#8a6a2a' } },
      }],
    });
    const ro = new ResizeObserver(() => { if (!inst.isDisposed()) inst.resize(); });
    ro.observe(ref.current);
    return () => { ro.disconnect(); inst.dispose(); };
  }, [byDay]);
  return <div ref={ref} style={{ width: '100%', height: 150 }} />;
}

// ── tile 詳情浮層 ──
function TileDetail({ stock, onClose }) {
  if (!stock) return null;
  return (
    <div className="qd-panel" style={{ marginTop: 14 }}>
      <div className="qd-panel-hd">
        <span>{stock.code}</span>
        <span className="kai">{stock.name}</span>
        <span style={{ marginLeft: 'auto', display: 'flex', gap: 12, alignItems: 'baseline' }}>
          <span style={{ fontSize: 'var(--fs-sm)', fontStyle: 'normal' }} className={stock.chg > 0 ? 'qd-up' : stock.chg < 0 ? 'qd-dn' : 'qd-flat'}>
            {stock.price} ({fmtSign(stock.chg, '%')})
          </span>
          <span onClick={onClose} style={{ cursor: 'pointer', color: 'var(--term-dim)', fontStyle: 'normal' }}>✕</span>
        </span>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'minmax(180px, 240px) 1fr', gap: 16, alignItems: 'center' }}>
        <div>
          {stock.five ? <FiveRadar five={stock.five} /> :
            <div style={{ fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', color: 'var(--term-dim)', padding: 20 }}>五力資料明日起提供</div>}
          <div style={{ textAlign: 'center', fontFamily: 'var(--font-en)', fontSize: 'var(--fs-xs)', color: 'var(--term-gold)' }}>
            綜合 {stock.composite} / 100{stock.five ? ` · 五力 ${stock.five.total}/67` : ''}
          </div>
        </div>
        <div>
          <div style={{ fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', color: 'var(--term-dim)', marginBottom: 6 }}>研判依據</div>
          {stock.reasons && stock.reasons.length ? (
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
              {stock.reasons.map((r, i) => <span key={i} className="qd-chip">{r}</span>)}
            </div>
          ) : <div style={{ fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', color: 'var(--term-dim)' }}>近 5 日無策略訊號，依五力/董事會綜合評分。</div>}
          {stock.chairmanSummary && (
            <div style={{ fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', color: 'var(--term-text)', marginTop: 10, lineHeight: 1.8, borderLeft: '2px solid var(--term-line)', paddingLeft: 10 }}>
              董事會主席：「{stock.chairmanSummary}」
            </div>
          )}
          <a href={'個股技術分析.html?code=' + stock.code}
             style={{ display: 'inline-block', marginTop: 12, fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', color: 'var(--term-gold)', textDecoration: 'none', borderBottom: '1px dotted var(--term-gold)' }}>
            → 開個股工作台驗證
          </a>
        </div>
      </div>
    </div>
  );
}

// ── 大盤數字 count-up（首次載入啟動一次，之後即時值直接寫）──
function CountUpNum({ value, className }) {
  const ref = useRef(null);
  const done = useRef(false);
  useEffect(() => {
    if (value == null) return;
    const el = ref.current;
    if (!el) return;
    const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (done.current || reduce || !window.anime) {
      el.textContent = Math.round(value).toLocaleString();
      done.current = true;
      return;
    }
    done.current = true;
    const obj = { v: 0 };
    window.anime({
      targets: obj, v: value, duration: 1100, easing: 'easeOutCubic',
      update: () => { el.textContent = Math.round(obj.v).toLocaleString(); },
    });
  }, [value]);
  return <div className={className} ref={ref}>{value != null ? Math.round(value).toLocaleString() : '—'}</div>;
}

// ── 研判關聯圖：力導向網路（force-graph 引擎；真資料=候選股×產業×今日漲跌）──
//   節點大小=綜合分、顏色=今日真實漲跌(diverging 紅漲/灰平/綠跌)、同產業稀疏鏈相連、
//   金環=今日精選。d3-force 收斂動畫；點節點下鑽五力詳情。
function nodeChgColor(chg) {
  if (chg == null) return '#b9b19c';
  if (chg > 0.3) return chg > 3 ? '#f4756c' : '#e0564f';    // 紅=漲
  if (chg < -0.3) return chg < -3 ? '#57c295' : '#3fa17a';  // 綠=跌
  return '#b9b19c';                                          // 平=暖灰
}
function SignalNetwork({ stocks, topPicks, industryMap, onPick }) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el || !window.ForceGraph || !stocks || !stocks.length) return;
    const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    const pickSet = {}; (topPicks || []).forEach(p => { pickSet[p.code] = 1; });
    const compOf = {}; stocks.forEach(s => { compOf[s.code] = s.composite; });
    const sectors = {};
    const nodes = stocks.map(s => {
      const sec = industryMap[s.code] || '其他';
      (sectors[sec] = sectors[sec] || []).push(s.code);
      return { id: s.code, name: s.name, composite: s.composite, chg: s.chg, sector: sec,
        isPick: !!pickSet[s.code], val: Math.max(1.2, (s.composite || 0) / 12), color: nodeChgColor(s.chg) };
    });
    const links = [];
    Object.keys(sectors).forEach(sec => {                     // 同產業依綜合分排序後相鄰相連（稀疏鏈）
      const arr = sectors[sec].slice().sort((a, b) => (compOf[b] || 0) - (compOf[a] || 0));
      for (let i = 0; i < arr.length - 1; i++) links.push({ source: arr[i], target: arr[i + 1] });
    });
    el.innerHTML = '';
    let fitted = false;
    const Graph = window.ForceGraph()(el)
      .backgroundColor('rgba(0,0,0,0)')
      .graphData({ nodes, links })
      .nodeId('id').nodeRelSize(3.4).nodeVal('val')
      .nodeLabel(n => `${n.id} ${n.name}　綜合 ${n.composite} · ${fmtSign(n.chg, '%')}　${n.sector}`)
      .linkColor(() => 'rgba(212,179,112,.20)').linkWidth(1)
      .nodeCanvasObject((n, ctx, scale) => {
        const r = 3.4 * Math.sqrt(n.val);
        ctx.beginPath(); ctx.arc(n.x, n.y, r, 0, 2 * Math.PI);
        ctx.fillStyle = n.color; ctx.fill();
        if (n.isPick) { ctx.strokeStyle = '#d4b370'; ctx.lineWidth = 2 / scale; ctx.stroke(); }
        if (n.composite >= 62) {
          ctx.font = `${11 / scale}px 'Times New Roman',serif`;
          ctx.fillStyle = '#d8cdb0'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
          ctx.fillText(n.id, n.x, n.y - r - 2 / scale);
        }
      })
      .onNodeClick(n => { if (onPick) onPick(n.id); })
      .cooldownTicks(reduce ? 0 : 140)
      .d3AlphaDecay(0.023).d3VelocityDecay(0.34)
      .onEngineStop(() => { if (!fitted) { fitted = true; Graph.zoomToFit(500, 46); } });
    Graph.d3Force('charge').strength(-340).distanceMax(520);
    Graph.d3Force('link').distance(58);
    // 防重疊：自訂碰撞力（force-graph 未內建 forceCollide，手刻簡易版）
    Graph.d3Force('collide', function (alpha) {
      var arr = Graph.graphData().nodes, n = arr.length;
      for (var i = 0; i < n; i++) for (var j = i + 1; j < n; j++) {
        var a = arr[i], b = arr[j];
        var ra = 3.4 * Math.sqrt(a.val) + 4, rb = 3.4 * Math.sqrt(b.val) + 4;
        var dx = (b.x || 0) - (a.x || 0), dy = (b.y || 0) - (a.y || 0);
        var d = Math.sqrt(dx * dx + dy * dy) || 0.01, min = ra + rb;
        if (d < min) {
          var push = (min - d) / d * alpha * 0.7, ox = dx * push, oy = dy * push;
          a.vx -= ox; a.vy -= oy; b.vx += ox; b.vy += oy;
        }
      }
    });
    const setSize = () => { Graph.width(el.clientWidth).height(460); };
    setSize();
    const ro = new ResizeObserver(setSize); ro.observe(el);
    return () => { ro.disconnect(); try { Graph._destructor && Graph._destructor(); } catch (e) {} el.innerHTML = ''; };
  }, [stocks, topPicks, industryMap]);
  return <div ref={ref} style={{ width: '100%', height: 460 }} />;
}

// ── 五力分布山脊圖（真資料 = 34 候選股×五力維度分布；紙張區）──
//   5 條山脊=趨勢/動能/籌碼/財務/估值，各顯示候選股在該力的分布密度，
//   看今日候選整體「哪個力強/弱、集中或分散」。功能：診斷選股的五力結構。
function FiveForceRidgeline({ stocks }) {
  const ref = useRef(null);
  useEffect(() => {
    if (!ref.current || typeof echarts === 'undefined' || !stocks || !stocks.length) return;
    const inst = echarts.init(ref.current, 'formosa', { renderer: 'svg' });
    const dims = [
      { key: 'pTrend', max: 25, name: '趨勢' }, { key: 'pMom', max: 20, name: '動能' },
      { key: 'pChip', max: 15, name: '籌碼' }, { key: 'pQual', max: 20, name: '財務' },
      { key: 'pVal', max: 20, name: '估值' },
    ];
    const BINS = 14, GAP = 34, H = 30, yMax = dims.length * GAP;
    const series = [];
    dims.forEach((d, i) => {
      const vals = stocks.map(s => s.five && s.five[d.key] != null ? (s.five[d.key] / d.max) * 100 : null).filter(v => v != null);
      const hist = new Array(BINS).fill(0);
      vals.forEach(v => { hist[Math.min(BINS - 1, Math.floor(v / 100 * BINS))]++; });
      const sm = hist.map((_, j) => ((hist[j - 1] || 0) + 2 * hist[j] + (hist[j + 1] || 0)) / 4);
      const mx = Math.max(...sm, 1);
      const off = (dims.length - 1 - i) * GAP;   // 趨勢在最上
      series.push({
        type: 'line', smooth: true, symbol: 'none', z: i, silent: false,
        lineStyle: { color: '#8a6a2a', width: 1 },
        areaStyle: {
          origin: off,
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: 'rgba(184,146,77,.55)' }, { offset: 1, color: 'rgba(184,146,77,.04)' }]),
        },
        data: sm.map((h, j) => [(j + 0.5) / BINS * 100, off + h / mx * H]),
      });
    });
    inst.setOption({
      grid: { left: 54, right: 16, top: 8, bottom: 30 },
      graphic: dims.map((d, i) => ({
        type: 'text', left: 8, top: (5 + i * 17.5) + '%',
        style: { text: d.name, fill: '#8a6a2a', font: "12.5px 'BiauKai','標楷體',serif" },
      })),
      xAxis: {
        type: 'value', min: 0, max: 100, name: '力度 %', nameLocation: 'middle', nameGap: 20,
        nameTextStyle: { fontSize: 11.5, color: '#9a9789', fontFamily: "'BiauKai','標楷體',serif" },
        axisLabel: { fontSize: 11.5, color: '#9a9789', fontFamily: "'Times New Roman',serif" },
        splitLine: { show: false }, axisTick: { show: false }, axisLine: { lineStyle: { color: '#d8cdb0' } },
      },
      yAxis: { type: 'value', min: 0, max: yMax, show: false },
      series,
    });
    const ro = new ResizeObserver(() => { if (!inst.isDisposed()) inst.resize(); });
    ro.observe(ref.current);
    return () => { ro.disconnect(); inst.dispose(); };
  }, [stocks]);
  return <div ref={ref} style={{ width: '100%', height: 220 }} />;
}

// ── 五力之門高爾頓板（真資料 = 候選股穿過五個力量之門；canvas 動畫）──
//   5 排釘=趨勢/動能/籌碼/財務/估值；每顆球=一檔候選股，在每排依「該力是否達門檻(60%)」
//   真實往左(不過)/右(過)落，最終桶位=通過力數(0~5)。落點分布與各門過關數皆為真資料，
//   非亂數物理。功能：看候選整體強弱分布 + 哪個力是最大瓶頸。
const GATE_DIMS = [
  { key: 'pTrend', max: 25, name: '趨勢' }, { key: 'pMom', max: 20, name: '動能' },
  { key: 'pChip', max: 15, name: '籌碼' }, { key: 'pQual', max: 20, name: '財務' }, { key: 'pVal', max: 20, name: '估值' },
];
const GATE_PASS = 0.6;
function ForceGateGalton({ stocks }) {
  const ref = useRef(null);
  const rafRef = useRef(0);
  const summary = useMemo(() => {
    const withFive = stocks.filter(s => s.five);
    const passCount = GATE_DIMS.map(d => withFive.filter(s => s.five[d.key] != null && (s.five[d.key] / d.max) >= GATE_PASS).length);
    const allPass = withFive.filter(s => GATE_DIMS.every(d => s.five[d.key] != null && (s.five[d.key] / d.max) >= GATE_PASS)).length;
    let bi = 0; passCount.forEach((c, i) => { if (c < passCount[bi]) bi = i; });
    return { n: withFive.length, allPass, bottleneck: GATE_DIMS[bi].name, bottleneckN: passCount[bi] };
  }, [stocks]);
  useEffect(() => {
    const cv = ref.current;
    if (!cv || !stocks || !stocks.length) return;
    const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    const balls = stocks.filter(s => s.five).map((s, idx) => {
      const rightsCum = []; let r = 0;
      GATE_DIMS.forEach(d => { if (s.five[d.key] != null && (s.five[d.key] / d.max) >= GATE_PASS) r++; rightsCum.push(r); });
      const tier = s.composite >= 70 ? 0 : s.composite >= 60 ? 1 : s.composite >= 45 ? 2 : 3;
      return { rightsCum, rights: r, tier, idx };
    });
    const binCounts = new Array(GATE_DIMS.length + 1).fill(0);
    balls.forEach(b => binCounts[b.rights]++);
    const tierColors = ['#b8924d', '#c9873f', '#8b91a5', '#bcb4a1'];
    const ctx = cv.getContext('2d');
    let W = 0, H = 0; const dpr = Math.min(window.devicePixelRatio || 1, 2);
    const PADL = 56, PADR = 16, PADT = 16, binH = 60;
    let boardTop, boardBot, cx, half;
    function geom() {
      W = cv.clientWidth; H = cv.clientHeight; cv.width = W * dpr; cv.height = H * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      boardTop = PADT; boardBot = H - binH - 26;
      cx = PADL + (W - PADL - PADR) / 2; half = Math.min((W - PADL - PADR) / 12, 46);
    }
    const levelY = i => boardTop + (i + 0.5) / GATE_DIMS.length * (boardBot - boardTop);
    const xAt = (rights, level) => cx + (2 * rights - level) * half;
    const binX = b => cx + (2 * b - GATE_DIMS.length) * half;
    function pathOf(b) {
      const pts = [{ x: cx, y: boardTop }];
      for (let i = 0; i < GATE_DIMS.length; i++) pts.push({ x: xAt(b.rightsCum[i], i + 1), y: levelY(i) });
      pts.push({ x: binX(b.rights), y: boardBot + 2 });
      return pts;
    }
    function draw(t) {
      ctx.clearRect(0, 0, W, H);
      ctx.fillStyle = '#d8cdb0';
      for (let i = 0; i < GATE_DIMS.length; i++) { const y = levelY(i); for (let r = 0; r <= i + 1; r++) { ctx.beginPath(); ctx.arc(xAt(r, i + 1), y, 2, 0, 7); ctx.fill(); } }
      ctx.fillStyle = '#8a6a2a'; ctx.font = "12.5px 'BiauKai','標楷體',serif"; ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
      GATE_DIMS.forEach((d, i) => ctx.fillText(d.name, PADL - 10, levelY(i)));
      const maxBin = Math.max(...binCounts, 1);
      for (let b = 0; b <= GATE_DIMS.length; b++) {
        const x = binX(b), c = binCounts[b], bh = c / maxBin * binH;
        ctx.fillStyle = 'rgba(184,146,77,.14)'; ctx.fillRect(x - half + 3, boardBot + 6, half * 2 - 6, binH);
        ctx.fillStyle = 'rgba(184,146,77,.5)'; ctx.fillRect(x - half + 3, boardBot + 6 + binH - bh, half * 2 - 6, bh);
        ctx.fillStyle = '#6a6f80'; ctx.font = "11.5px 'Times New Roman',serif"; ctx.textAlign = 'center'; ctx.textBaseline = 'top';
        ctx.fillText(b + '力', x, boardBot + 6 + binH + 4);
        ctx.fillStyle = '#8a6a2a'; ctx.fillText(String(c), x, boardBot - 10);
      }
      balls.forEach(b => {
        const delay = b.idx * (reduce ? 0 : 52), dur = reduce ? 0 : 660;
        const p = reduce ? 1 : Math.max(0, Math.min(1, (t - delay) / dur));
        if (p <= 0) return;
        const pts = pathOf(b), segs = pts.length - 1, fp = p * segs, si = Math.min(segs - 1, Math.floor(fp)), lt = fp - si;
        const a = pts[si], c2 = pts[si + 1];
        ctx.beginPath(); ctx.arc(a.x + (c2.x - a.x) * lt, a.y + (c2.y - a.y) * lt, 4, 0, 7);
        ctx.fillStyle = tierColors[b.tier]; ctx.fill();
      });
    }
    geom();
    let start = null;
    const total = reduce ? 0 : balls.length * 52 + 660 + 120;
    function frame(ts) { if (start == null) start = ts; const t = ts - start; draw(t); if (t < total) rafRef.current = requestAnimationFrame(frame); else draw(1e9); }
    if (reduce) draw(1e9); else rafRef.current = requestAnimationFrame(frame);
    const ro = new ResizeObserver(() => { geom(); draw(1e9); });
    ro.observe(cv);
    return () => { cancelAnimationFrame(rafRef.current); ro.disconnect(); };
  }, [stocks]);
  return (
    <div>
      <canvas ref={ref} style={{ width: '100%', height: 340, display: 'block' }} />
      <div style={{ fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', color: 'var(--ink-mute)', marginTop: 6, letterSpacing: '.02em' }}>
        {summary.n} 檔候選穿過五力之門（門檻＝該力達 60%）：<b style={{ color: 'var(--gold-deep)' }}>{summary.allPass} 檔五力全過</b>（落最右桶＝最強）；
        最大瓶頸＝<b style={{ color: 'var(--ink)' }}>{summary.bottleneck}</b>（僅 {summary.bottleneckN} 檔通過）。球色＝綜合分層級。
      </div>
    </div>
  );
}

// ── 機構覆蓋段落（原雷達區獨立分頁收編；展開才載 institutional-research-db 230KB）──
function CoverageSection() {
  const [state, setState] = useState('closed'); // closed | loading | open | error
  const open = () => {
    if (state === 'open' || state === 'loading') return;
    setState('loading');
    (window.loadScript ? window.loadScript('institutional-research-db.js') : Promise.resolve())
      .then(() => window.loadJsx ? window.loadJsx('coverage-list.jsx') : Promise.resolve())
      .then(() => setState('open'))
      .catch(() => setState('error'));
  };
  return (
    <div style={{ marginBottom: 26 }}>
      <window.SectionHead kicker="Coverage" title="機構覆蓋" titleEn="Institutional Coverage"
        sub="12 檔深度研究：評等/DCF 情境/可比公司/催化劑——策展研究內容，非自動生成" />
      {state === 'closed' && (
        <button onClick={open} className="lab-btn ghost" style={{ fontSize: 'var(--fs-xs)' }}>
          ▾ 展開覆蓋總表（首次展開載入研究庫）
        </button>
      )}
      {state === 'loading' && <div style={{ fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', color: 'var(--ink-mute)', padding: '10px 0' }}>研究庫載入中…</div>}
      {state === 'error' && <div style={{ fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', color: 'var(--bear)' }}>⚠ 載入失敗，請重新整理再試。</div>}
      {state === 'open' && window.CoverageListApp && <window.CoverageListApp embedded />}
    </div>
  );
}

// ── 主 App ──
function BrainDeskApp({ embedded }) {
  const [brain, setBrain] = useState(null);
  const [stats, setStats] = useState(null);
  const [err, setErr] = useState('');
  const [selCode, setSelCode] = useState(null);
  const [industryMap, setIndustryMap] = useState({});

  useEffect(() => {
    let dead = false;
    fetch('/api/stock?type=brain').then(r => r.json())
      .then(d => { if (!dead) d.error ? setErr(d.error) : setBrain(d); })
      .catch(e => { if (!dead) setErr(e.message); });
    fetch('/api/stock?type=signal_stats').then(r => r.json())
      .then(d => { if (!dead && !d.error) setStats(d); })
      .catch(() => {});
    fetch('/industry_codes.json').then(r => r.json())
      .then(m => { if (!dead && m && typeof m === 'object') setIndustryMap(m); })
      .catch(() => {});
    return () => { dead = true; };
  }, []);

  // TAIEX 即時（QuoteHub 訂閱制；收不到時用 brain.taiex 靜態值）
  const rt = window.useQuote ? window.useQuote({ type: 'taiex' }) : { data: null };
  const live = useMemo(() => {
    const items = rt.data?.msgArray || [];
    const t = items.find(i => i.c === 't00' || i.ex === 'tse');
    if (!t) return null;
    const z = t.z && t.z !== '-' ? parseFloat(t.z) : NaN;
    const y = parseFloat(t.y || '0');
    const val = !isNaN(z) && z > 0 ? z : y;
    if (!(val > 0) || !(y > 0)) return null;
    return { close: val, chg: +((val / y - 1) * 100).toFixed(2) };
  }, [rt.data]);

  const taiex = live || brain?.taiex || null;
  const st = STANCE_STYLE[brain?.stance] || {};
  const stocks = brain?.stocks || [];

  // Desk Strip：發佈研判中樞關鍵數字到四分頁共用狀態帶
  useEffect(() => {
    if (!window.QhStrip || !brain) return;
    const b = brain.breadth || {};
    const items = [];
    if (taiex) items.push({ label: '大盤', value: taiex.close.toLocaleString(undefined, { maximumFractionDigits: 0 }), sub: fmtSign(taiex.chg, '%'), tone: taiex.chg > 0 ? 'up' : taiex.chg < 0 ? 'dn' : 'flat' });
    if (brain.stance) items.push({ label: '姿態', value: brain.stance, tone: 'gold' });
    if (brain.confidence != null) items.push({ label: '同向度', value: Math.round(brain.confidence) + '%' });
    if (b.strongN != null) items.push({ label: '強勢股', value: b.strongN + ' / ' + (b.universe != null ? b.universe : '—') });
    if (items.length) window.QhStrip.set('desk', items);
  }, [brain, taiex]);
  // C1-info：今日精選(topPicks)產業分布（純資訊，不警示——候選池本就科技集中）
  const pickSectors = useMemo(() => {
    const picks = brain?.topPicks || [];
    if (!picks.length) return [];
    const by = {};
    picks.forEach(p => { const sec = industryMap[p.code] || '其他'; by[sec] = (by[sec] || 0) + 1; });
    return Object.keys(by).map(k => ({ sector: k, n: by[k] })).sort((a, b) => b.n - a.n);
  }, [brain, industryMap]);
  const selStock = stocks.find(s => s.code === selCode) || null;
  const lampOf = c => c >= 70 ? 'gold' : c >= 60 ? 'lit' : c >= 45 ? 'dim' : 'off';

  // 分享卡：此頁 host 未載 share-card.js，首次點擊才注入
  const ensureShareCard = () => window.ShareCard ? Promise.resolve() : new Promise((res, rej) => {
    const s = document.createElement('script');
    s.src = 'share-card.js'; s.onload = res; s.onerror = rej;
    document.head.appendChild(s);
  });
  const shareCard = (kind) => {
    if (!brain) return;
    ensureShareCard().then(() => {
      if (kind === 'picks') window.ShareCard.openPicksCard(brain);
      else window.ShareCard.openBrainCard(brain);
    }).catch(() => {});
  };
  const shareBtnStyle = {
    fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', padding: '5px 13px',
    border: '1px solid var(--term-gold)', borderRadius: 4, background: 'transparent',
    color: 'var(--term-gold)', cursor: 'pointer', letterSpacing: '.04em',
  };
  const trackedDays = stats?.byDay?.length || 0;
  const filledAny = (stats?.byStrategy || []).some(r => r.filled > 0);

  return (
    <div className={embedded ? '' : 'shell'}>
      {!embedded && <window.Masthead />}
      {!embedded && <window.TickerTape />}
      {!embedded && <window.NavBar active="radarzone" />}

      <section style={{ paddingTop: embedded ? 22 : 30 }}>
        {/* ① 終端 hero */}
        <div className="qd-term">
          {err && <div style={{ fontFamily: 'var(--font-cn)', color: 'var(--term-up)', fontSize: 'var(--fs-sm)' }}>⚠ {err}</div>}
          {brain && !brain.computing && brain.stance && (
            <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginBottom: 12 }}>
              <button onClick={() => shareCard('brain')} style={shareBtnStyle} title="產生今日研判分享卡（存成圖片）">🎴 研判卡</button>
              {stocks.length > 0 && brain.topPicks && brain.topPicks.length > 0 && (
                <button onClick={() => shareCard('picks')} style={shareBtnStyle} title="產生今日精選分享卡（存成圖片）">🎴 精選卡</button>
              )}
            </div>
          )}
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 26, alignItems: 'center' }}>
            {/* 大盤 mega 數字 */}
            <div className="qd-stat" style={{ textAlign: 'left', flex: '1 1 300px' }}>
              <div style={{ fontFamily: 'var(--font-en)', fontStyle: 'italic', fontSize: 'var(--fs-nano)', letterSpacing: '.16em', color: 'var(--term-dim)', textTransform: 'uppercase' }}>
                Taiwan Weighted Index · {brain?.date || ''}{brain?.stale ? ' (昨日研判)' : ''}
              </div>
              <CountUpNum className="qd-stat-num mega led" value={taiex ? taiex.close : null} />
              <div style={{ fontFamily: 'var(--font-en)', fontSize: 'var(--fs-md)', fontVariantNumeric: 'tabular-nums' }}
                   className={taiex && taiex.chg > 0 ? 'qd-up' : taiex && taiex.chg < 0 ? 'qd-dn' : 'qd-flat'}>
                {taiex ? fmtSign(taiex.chg, '%') : ''}
                {brain?.taiex?.ma20 && (
                  <span style={{ fontSize: 'var(--fs-xs)', color: 'var(--term-dim)', marginLeft: 12 }}>
                    MA20 {brain.taiex.ma20.toLocaleString()}
                    {brain?.regime ? (
                      <span style={{ color: (REGIME_STYLE[brain.regime.label] || {}).color || 'var(--term-flat)', fontFamily: 'var(--font-cn)', marginLeft: 8 }}>
                        · {brain.regime.label} · ER {brain.regime.er}
                      </span>
                    ) : (
                      <span style={{ marginLeft: 8 }}>· {brain.taiex.aboveMA20 ? '線上' : '線下'}</span>
                    )}
                  </span>
                )}
              </div>
            </div>

            {/* 研判大印 + 儀表 */}
            <div style={{ display: 'flex', gap: 20, alignItems: 'center' }}>
              <div style={{
                fontFamily: 'var(--font-cn)', fontSize: 46, lineHeight: 1.1,
                color: st.color || 'var(--term-dim)',
                border: '2px solid currentColor', borderRadius: 3,
                padding: '14px 18px 10px',
                letterSpacing: '.14em', position: 'relative',
              }}>
                {brain?.stance || '…'}
                <div style={{ fontSize: 'var(--fs-nano)', fontFamily: 'var(--font-en)', fontStyle: 'italic', letterSpacing: '.08em', textAlign: 'center', marginTop: 2 }}>{st.label || ''}</div>
              </div>
              <div>
                <ConfidenceGauge value={brain?.confidence} stance={brain?.stance} />
                <div style={{ textAlign: 'center', fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-nano)', color: 'var(--term-dim)', marginTop: -8 }}>四來源同向度</div>
              </div>
            </div>

            {/* 廣度 chips */}
            {brain?.breadth && (
              <div style={{ flex: '1 1 220px', display: 'flex', flexDirection: 'column', gap: 7 }}>
                {[
                  ['策略訊號', `${brain.breadth.labHits} / ${brain.breadth.universe} 檔`],
                  ['強勢股(≥60分)', `${brain.breadth.strongN} 檔 (${Math.round(brain.breadth.breadthRatio * 100)}%)`],
                  ['董事會 多/空', `${brain.breadth.boardBull} / ${brain.breadth.boardBear}`],
                  ['平均五力', brain.breadth.avgFive != null ? brain.breadth.avgFive + ' / 67' : '明日起'],
                ].map(([k, v]) => (
                  <div key={k} style={{ display: 'flex', justifyContent: 'space-between', gap: 10, borderBottom: '1px solid rgba(139,145,165,.14)', paddingBottom: 4 }}>
                    <span style={{ fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', color: 'var(--term-dim)' }}>{k}</span>
                    <span style={{ fontFamily: 'var(--font-en)', fontSize: 'var(--fs-xs)', color: 'var(--term-text)', fontVariantNumeric: 'tabular-nums' }}>{v}</span>
                  </div>
                ))}
              </div>
            )}
          </div>

          {brain?.caution && (
            <div style={{ marginTop: 14, padding: '9px 16px', borderRadius: 'var(--radius-sm)', background: 'rgba(224,86,79,.10)', border: '1px solid rgba(224,86,79,.45)', fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-sm)', color: '#eb8f89', letterSpacing: '.04em' }}>
              ⚠ 今日不宜進場——{brain?.regime?.label === '空頭' ? '大盤空頭趨勢，順勢做多勝率低' : '盤整格局且強勢股稀少，觀望為宜'}
            </div>
          )}

          {brain?.chairmanQuote && (
            <div style={{ marginTop: 16, fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-sm)', color: 'var(--term-text)', lineHeight: 1.8, borderTop: '1px solid var(--term-line)', paddingTop: 12 }}>
              <span style={{ color: 'var(--term-gold)' }}>董事會主席</span>：「{brain.chairmanQuote}」
            </div>
          )}
          {brain?.computing && (
            <div style={{ marginTop: 10, fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', color: 'var(--term-dim)' }}>
              今日研判計算中——每次造訪推進一批，或等午夜自動完成。{brain.note || ''}
            </div>
          )}

          {/* ①.5 研判關聯圖（力導向網路，點節點下鑽五力詳情） */}
          {stocks.length > 1 && (
            <div style={{ marginTop: 22 }}>
              <div className="qd-panel-hd" style={{ marginBottom: 4 }}>
                <span>Signal Relationship Map</span><span className="kai">研判關聯圖</span>
                <span style={{ marginLeft: 'auto', fontStyle: 'normal', fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-nano)', color: 'var(--term-dim)' }}>
                  節點大小=綜合分 · 顏色=今日漲跌(紅漲綠跌) · 同產業相連 · 金環=今日精選 · 可拖曳/縮放 · 點看五力
                </span>
              </div>
              <SignalNetwork stocks={stocks} topPicks={brain?.topPicks} industryMap={industryMap} onPick={setSelCode} />
            </div>
          )}

          {/* ② 訊號燈牆 */}
          {stocks.length > 0 && (
            <div style={{ marginTop: 20 }}>
              <div className="qd-panel-hd" style={{ marginBottom: 8 }}>
                <span>Signal Wall</span><span className="kai">訊號燈牆</span>
                <span style={{ marginLeft: 'auto', fontStyle: 'normal', fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-nano)', color: 'var(--term-dim)' }}>
                  ● 金=強(≥70) 紅=有訊號(≥60) 灰=觀望(≥45) · 點格看研判依據
                </span>
              </div>
              {pickSectors.length > 0 && (
                <div style={{ marginBottom: 10, fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', color: 'var(--term-dim)' }}>
                  今日精選產業：{pickSectors.map(s => s.sector + ' ' + s.n).join(' · ')}
                </div>
              )}
              <div className="qd-wall">
                {stocks.map(s => (
                  <div key={s.code} className="qd-tile" onClick={() => setSelCode(prev => prev === s.code ? null : s.code)}>
                    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
                      <span className="code">{s.code}</span>
                      <span className={'qd-lamp ' + lampOf(s.composite)} />
                    </div>
                    <div className="name">{s.name}</div>
                    <div className="chg" style={{ display: 'flex', justifyContent: 'space-between' }}>
                      <span className={s.chg > 0 ? 'qd-up' : s.chg < 0 ? 'qd-dn' : 'qd-flat'}>{fmtSign(s.chg, '%')}</span>
                      <span style={{ color: 'var(--term-gold)' }}>{s.composite}</span>
                    </div>
                  </div>
                ))}
              </div>
              <TileDetail stock={selStock} onClose={() => setSelCode(null)} />
            </div>
          )}
        </div>

        {/* ②.5 五力分布山脊圖（紙張區） */}
        {stocks.length > 1 && (
          <div style={{ marginBottom: 26 }}>
            <window.SectionHead kicker="Five-Force Ridgeline" title="五力分布" titleEn="Five-Force Ridgeline"
              sub="今日候選股在五個力的分布密度——山脊靠右=該力整體強、脊峰窄=分布集中、雙峰=兩極分化" />
            <FiveForceRidgeline stocks={stocks} />
          </div>
        )}

        {/* ②.6 五力之門高爾頓板（紙張區） */}
        {stocks.length > 1 && (
          <div style={{ marginBottom: 26 }}>
            <window.SectionHead kicker="Force Gate Lattice" title="五力之門" titleEn="Force Gate Lattice"
              sub="每檔候選股穿過五個力量之門，依實際是否通過往左/右落——落點=通過力數，看整體強弱分布與最大瓶頸" />
            <ForceGateGalton stocks={stocks} />
          </div>
        )}

        {/* ③ 命中率追蹤（紙張區） */}
        <window.SectionHead kicker="Track Record" title="命中率追蹤" titleEn="Signal Track Record"
          sub="每個訊號都有案底：訊號日後 5/10 個交易日的實際報酬自動回填，不是回測，是實盤追蹤" />
        {!stats || trackedDays === 0 ? (
          <div style={{ padding: '26px 22px', border: '1px dashed var(--paper-line)', textAlign: 'center', fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-sm)', color: 'var(--ink-mute)', marginBottom: 26 }}>
            訊號史庫累積中——今天是第 {trackedDays || 1} 天。訊號滿 15 天後開始回填實際報酬，命中率曲線會在這裡長出來。
          </div>
        ) : (
          <div style={{ marginBottom: 26 }}>
            <div style={{ fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', color: 'var(--ink-mute)', marginBottom: 4 }}>
              每日訊號密度（近 {trackedDays} 個掃描日）
            </div>
            {trackedDays >= 2
              ? <SignalDensityChart byDay={stats.byDay} />
              : <div style={{ fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', color: 'var(--ink-mute)', padding: '8px 0 16px' }}>史庫第 {trackedDays} 天——密度圖需要至少 2 個掃描日。</div>}
            <div style={{ overflowX: 'auto', marginTop: 14 }}>
              <table className="lab-table">
                <thead><tr><th>來源 / 策略</th><th>累積訊號</th><th>已回填</th><th>+10日勝率</th><th>+5日均報</th><th>+10日均報</th></tr></thead>
                <tbody>
                  {(stats.byStrategy || []).map(r => (
                    <tr key={r.source + r.strategy_id}>
                      <td style={{ fontFamily: 'var(--font-cn)' }}>{r.source === 'five_force' ? '五力評分快照' : (window.STRATEGY_LIB || []).find(s => s.id === r.strategy_id)?.name.cn || r.strategy_id}</td>
                      <td>{r.total}</td>
                      <td>{r.filled}</td>
                      <td style={{ color: r.winRate10 == null ? 'var(--ink-mute)' : r.winRate10 >= 50 ? 'var(--bull)' : 'var(--bear)' }}>{r.winRate10 == null ? '回填中' : r.winRate10 + '%'}</td>
                      <td>{r.avg5 == null ? '—' : fmtSign(r.avg5, '%')}</td>
                      <td>{r.avg10 == null ? '—' : fmtSign(r.avg10, '%')}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
              {!filledAny && (
                <div style={{ fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-nano)', color: 'var(--ink-mute)', marginTop: 6 }}>
                  ※ 尚無回填數據（訊號需滿 15 天）——「勝率」欄目前是誠實的空白，不是壞掉。
                </div>
              )}
            </div>
          </div>
        )}

        {/* ③.5 機構覆蓋（原獨立分頁收編：點開才載 230KB 研究庫） */}
        <CoverageSection />

        {/* ③.6 The Firm 入口卡（金融團隊退出 tab，保留獨立頁） */}
        <a href="AI金融團隊.html" style={{ textDecoration: 'none', display: 'block', marginBottom: 30 }}>
          <div style={{
            display: 'flex', alignItems: 'center', gap: 16,
            border: '1px solid var(--paper-line)', borderLeft: '3px solid var(--gold)',
            background: 'var(--paper-bright)', padding: '14px 20px', cursor: 'pointer',
          }}>
            {window.FirmSealIcon && <window.FirmSealIcon size={44} color="var(--gold-deep)" strokeWidth={1.3} />}
            <div style={{ flex: 1 }}>
              <div style={{ fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-md)', color: 'var(--ink)' }}>
                AI 金融團隊 <span style={{ fontFamily: 'var(--font-en)', fontStyle: 'italic', fontSize: 'var(--fs-xs)', color: 'var(--gold-deep)' }}>The Firm</span>
              </div>
              <div style={{ fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', color: 'var(--ink-mute)', marginTop: 3 }}>
                七部門編制、每日產出登記簿、42 位專員的建議與工作區——本頁研判就是他們的合議結果。
              </div>
            </div>
            <span style={{ color: 'var(--gold-deep)', fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-sm)' }}>入內 →</span>
          </div>
        </a>

        {/* ④ 紙張收尾：方法論 */}
        <window.SectionHead kicker="Methodology" title="這顆大腦怎麼想事情" titleEn="How The Desk Thinks" />
        <div style={{ fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-sm)', lineHeight: 'var(--lh-body)', color: 'var(--ink-soft)', maxWidth: 760, marginBottom: 8 }}>
          每天午夜，系統掃描 34 檔主力股：策略實驗室的五套規則找「最近 5 日誰觸發了訊號」（佔 30 分）、
          Minervini 五力評分量趨勢與動能（30 分）、AI 董事會的主席裁決（25 分）、券資比軋空榜（15 分）——
          四個來源加總成每檔的綜合分，也各投一票決定今日大盤姿態：進攻、中性、或防守——
          其中大盤方向票已由「TAIEX regime（多頭／盤整／空頭，以 MA20/MA60 結構＋Kaufman 效率比判定）」取代原本單純的 MA20 上下；盤整時該票棄權，且盤整盤即使個股廣泛強勢也不喊全力進攻（姿態壓回中性）。空頭、或盤整且強勢股稀少時，另跳出「今日不宜進場」提示。
          策略訊號那 30 分還會依 regime 溫和微調權重：盤整盤降權趨勢型策略、升權反轉型，多頭盤反之。所有計分規則都是確定性的，AI 只出現在董事會那一票，而且引用的是它每天既有的裁決，不另外燒任何額度。
        </div>
        <div style={{ fontFamily: 'var(--font-cn)', fontSize: 'var(--fs-xs)', color: 'var(--ink-mute)', lineHeight: 1.9, marginBottom: 30 }}>
          研判每日一次、盤後計算，不是即時操作訊號。一切內容僅供研究學習，不構成投資建議，本站不涉及任何真實下單。
        </div>
      </section>

      {!embedded && <window.FooterBar />}
    </div>
  );
}

window.BrainDeskApp = BrainDeskApp;

const _deskRoot = document.getElementById('desk-root');
if (_deskRoot) ReactDOM.createRoot(_deskRoot).render(<BrainDeskApp />);
