﻿// chart-pro.jsx — lightweight-charts 升級版
// kline-specialist · 前端工坊 B
// Repo: github.com/tradingview/lightweight-charts

const { useState, useRef, useMemo, useEffect } = React;

// ── Indicator Math ────────────────────────────────────────
function emaArr(arr, n) {
  const k = 2 / (n + 1);
  const p = Math.min(n, arr.length);
  let prev = arr.slice(0, p).reduce((a, b) => a + b, 0) / p;
  const out = [prev];
  for (let i = 1; i < arr.length; i++) {
    prev = arr[i] * k + prev * (1 - k);
    out.push(prev);
  }
  return out;
}
function smaArr(arr, n) {
  return arr.map((_, i) => {
    if (i < n - 1) return null;
    return arr.slice(i - n + 1, i + 1).reduce((a, b) => a + b, 0) / n;
  });
}
function macdCalc(closes) {
  const e12 = emaArr(closes, 12), e26 = emaArr(closes, 26);
  const dif = e12.map((v, i) => v - e26[i]);
  const dea = emaArr(dif, 9);
  const hist = dif.map((v, i) => (v - dea[i]) * 2);
  return { dif, dea, hist };
}
function rsiCalc(data, n = 14) {
  const out = []; let g = 0, l = 0;
  for (let i = 0; i < data.length; i++) {
    if (i === 0) { out.push(null); continue; }
    const d = data[i].close - data[i-1].close;
    const gn = Math.max(d, 0), ln = Math.max(-d, 0);
    if (i < n) { g += gn; l += ln; out.push(null); continue; }
    if (i === n) { g = (g + gn)/n; l = (l + ln)/n; }
    else { g = (g*(n-1)+gn)/n; l = (l*(n-1)+ln)/n; }
    out.push(100 - 100/(1 + (l===0 ? 100 : g/l)));
  }
  return out;
}
function bollingerCalc(closes, n = 20, k = 2) {
  const mid = smaArr(closes, n);
  return {
    mid,
    upper: mid.map((m, i) => {
      if (m == null) return null;
      const s = Math.sqrt(closes.slice(i-n+1, i+1).reduce((a, b) => a + (b-m)**2, 0) / n);
      return m + k * s;
    }),
    lower: mid.map((m, i) => {
      if (m == null) return null;
      const s = Math.sqrt(closes.slice(i-n+1, i+1).reduce((a, b) => a + (b-m)**2, 0) / n);
      return m - k * s;
    }),
  };
}

// ── CM_MacD_Ult_MTF 4-color histogram ────────────────────
function hist4Color(bar, prevBar) {
  if (bar > 0) return bar > prevBar ? '#26a69a' : '#b2dfdb';
  return bar > prevBar ? '#ef9a9a' : '#ef5350';
}

// ── TWSE 開市判斷（台灣時間 09:00–13:30，週一至週五）────────
function isTWSEOpen() {
  const now = new Date();
  const twNow = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Taipei' }));
  const day = twNow.getDay(); // 0=Sun, 6=Sat
  if (day === 0 || day === 6) return false;
  const h = twNow.getHours(), m = twNow.getMinutes();
  const mins = h * 60 + m;
  return mins >= 540 && mins <= 810; // 09:00–13:30
}

// ── 取台灣今日日期字串 YYYY-MM-DD ────────────────────────────
function getTodayTW() {
  const now = new Date();
  const twStr = now.toLocaleString('en-US', { timeZone: 'Asia/Taipei',
    year:'numeric', month:'2-digit', day:'2-digit' });
  // 格式: M/D/YYYY → 轉成 YYYY-MM-DD
  const [m, d, y] = twStr.split('/');
  return `${y}-${m.padStart(2,'0')}-${d.padStart(2,'0')}`;
}

// ── Date utility: 補缺失的 fullDate ────────────────────────
function buildTimeStr(d, idx, total) {
  if (d.fullDate && /^\d{4}-\d{2}-\d{2}$/.test(d.fullDate)) return d.fullDate;
  const dt = new Date();
  let offset = total - 1 - idx;
  while (offset > 0) {
    dt.setDate(dt.getDate() - 1);
    if (dt.getDay() !== 0 && dt.getDay() !== 6) offset--;
  }
  return dt.toISOString().slice(0, 10);
}

// ── OHLCV 聚合（日→週/月/季）────────────────────────────────
// ── Constants ─────────────────────────────────────────────
const TF_PRESETS = [
  { id:'3M',  days:66,   label:'3M'  },
  { id:'6M',  days:132,  label:'6M'  },
  { id:'1Y',  days:250,  label:'1Y'  },
  { id:'ALL', days:9999, label:'All' },
];
// MA 均線：週線(5日)、月線(20日)、季線(60日)、年線(240日)
const MA_LINES = [
  { id:'ma5',  n:5,   label:'週', cn:'週線MA5',   color:'#f9a825' },
  { id:'ma20', n:20,  label:'月', cn:'月線MA20',  color:'#ab47bc' },
  { id:'ma60', n:60,  label:'季', cn:'季線MA60',  color:'#26c6da' },
  { id:'ma240',n:240, label:'年', cn:'年線MA240', color:'#ff7043' },
];
const INDS = [
  { id:'ema',    label:'EMA',    cn:'指數均線',  overlay:true,  on:true  },
  { id:'bb',     label:'BBands', cn:'布林通道',  overlay:true,  on:false },
  { id:'volume', label:'Vol',    cn:'成交量',    overlay:false, on:true  },
  { id:'macd',   label:'MACD',   cn:'MACD 動能', overlay:false, on:true  },
  { id:'rsi',    label:'RSI',    cn:'RSI',      overlay:false, on:false },
];
// 2026-07-20 可讀性改版：主題改量化平台終端墨藍（--term-* 同源），文字提亮字級加大
const TV = {
  bg:'#0e1730', grid:'rgba(212,179,112,.10)', text:'#a9afbf', bright:'#e9e0c9',
  bull:'#26a69a', bear:'#ef5350',   // 預設（美股慣例：綠漲紅跌）
  ema20:'#ef9a6a', ema50:'#e6a817', ema100:'#4db6ac', ema200:'#7a9cc6',
  bb:'#ffb74d',
};

// ── ChartPro ──────────────────────────────────────────────
// twColors: 台股紅漲綠跌（個股技術分析頁傳入；美股頁不傳維持綠漲紅跌）
function ChartPro({ data, stock, rtQuote, dividends, financials, signalPrices, rangeTabs, twColors }) {
  const UP = twColors ? '#e0564f' : TV.bull;   // K棒/成交量 漲色
  const DN = twColors ? '#3fa17a' : TV.bear;   // 跌色
  const [tf,  setTf]  = useState('3M');
  const [ind, setInd] = useState(() => Object.fromEntries(INDS.map(i => [i.id, i.on])));
  const [maOn, setMaOn] = useState(false);
  const [showSigLines, setShowSigLines] = useState(true);

  const mainRef  = useRef(null);
  const ohlcRef  = useRef(null);   // 十字游標 OHLC 資訊卡
  const macdRef  = useRef(null);
  const rsiRef   = useRef(null);
  const chartsRef = useRef([]);

  // ── 拖曳測量工具(互動升級層 v2.5)──
  const [measOn, setMeasOn] = useState(false);
  const measLayerRef = useRef(null);
  const measBoxRef   = useRef(null);
  const measDrag     = useRef(null);
  const candleRef    = useRef(null);

  // Visible window
  const vis = useMemo(() => {
    const days = TF_PRESETS.find(p => p.id === tf)?.days ?? 66;
    return data.slice(-Math.min(days, data.length));
  }, [data, tf]);

  // Indicators on FULL data (warm up properly)
  const closes  = useMemo(() => data.map(d => d.close), [data]);
  const fEMA20  = useMemo(() => emaArr(closes, 20),  [closes]);
  const fEMA50  = useMemo(() => emaArr(closes, 50),  [closes]);
  const fEMA100 = useMemo(() => emaArr(closes, 100), [closes]);
  const fEMA200 = useMemo(() => emaArr(closes, 200), [closes]);
  const fBB     = useMemo(() => bollingerCalc(closes), [closes]);
  const fMACD   = useMemo(() => macdCalc(closes), [closes]);
  const fRSI    = useMemo(() => rsiCalc(data), [data]);
  // SMA 均線
  const fMAs    = useMemo(() => Object.fromEntries(MA_LINES.map(m => [m.id, smaArr(closes, m.n)])), [closes]);

  // Build unified time-indexed dataset
  const si = data.length - vis.length; // startIndex
  const lw = useMemo(() => {
    const base = vis.map((d, i) => {
      const gi = si + i;
      return {
        time:     buildTimeStr(d, i, vis.length),
        open:d.open, high:d.high, low:d.low, close:d.close, volume:d.volume||0,
        ema20:  fEMA20[gi],  ema50:  fEMA50[gi],
        ema100: fEMA100[gi], ema200: fEMA200[gi],
        bbU:fBB.upper[gi], bbM:fBB.mid[gi], bbL:fBB.lower[gi],
        dif:fMACD.dif[gi], dea:fMACD.dea[gi],
        hist:fMACD.hist[gi], prevHist:fMACD.hist[gi-1]??fMACD.hist[gi],
        rsi:fRSI[gi],
        ma5:fMAs.ma5[gi], ma20:fMAs.ma20[gi], ma60:fMAs.ma60[gi], ma240:fMAs.ma240[gi],
      };
    }).filter(d => d.close > 0);

    // ── 即時 K 棒：開市中且有效報價 ──────────────────────────
    if (isTWSEOpen() && rtQuote) {
      const parseMIS = v => { if (!v || v === '-' || v === '--') return 0; return parseFloat(v) || 0; };
      const rtO = parseMIS(rtQuote.o);
      const rtZ = parseMIS(rtQuote.z) || parseMIS(rtQuote.b); // z=成交價，b=最佳買價(fallback)
      const rtH = parseMIS(rtQuote.h);
      const rtL = parseMIS(rtQuote.l);
      const rtV = parseMIS(rtQuote.v);
      if (rtZ > 0 && rtO > 0) {
        const todayStr = getTodayTW();
        const lastBarTime = base.length > 0 ? base[base.length - 1].time : '';
        // 只在今日日期嚴格大於最後一根歷史 bar 時才附加（避免重複）
        if (todayStr > lastBarTime) {
          base.push({
            time: todayStr,
            open: rtO, high: rtH || rtZ, low: rtL || rtZ, close: rtZ,
            volume: rtV * 1000,
            ema20: null, ema50: null, ema100: null, ema200: null,
            bbU: null, bbM: null, bbL: null,
            dif: null, dea: null, hist: null, prevHist: null,
            rsi: null,
            isRtBar: true,
          });
        }
      }
    }

    return base;
  }, [vis, si, fEMA20,fEMA50,fEMA100,fEMA200,fBB,fMACD,fRSI, rtQuote]);

  // Build & teardown charts
  useEffect(() => {
    const LC = window.LightweightCharts;
    if (!LC || !mainRef.current || !lw.length) return;

    // Destroy previous
    chartsRef.current.forEach(c => { try { c.remove(); } catch(_) {} });
    chartsRef.current = [];

    const base = {
      layout: { background:{ color:TV.bg }, textColor:TV.text, fontSize: 13 },
      grid: { vertLines:{ color:TV.grid }, horzLines:{ color:TV.grid } },
      rightPriceScale: { borderColor:TV.grid },
      timeScale: { borderColor:TV.grid, fixLeftEdge:true, fixRightEdge:true, timeVisible:false },
      crosshair: { mode:1 },
    };

    // ── Main ─────────────────────────────────────────────
    const main = LC.createChart(mainRef.current, { ...base, height:450 });
    chartsRef.current.push(main);

    // Candles（UP/DN 依市場慣例：台股紅漲綠跌 / 美股綠漲紅跌）
    const candle = main.addCandlestickSeries({
      upColor:UP, downColor:DN,
      borderUpColor:UP, borderDownColor:DN,
      wickUpColor:UP, wickDownColor:DN,
    });
    candle.setData(lw.map(d => ({ time:d.time, open:d.open, high:d.high, low:d.low, close:d.close })));
    candleRef.current = candle;
    if (measBoxRef.current) measBoxRef.current.style.display = 'none'; // 換時間框清掉舊測量

    // ── 掃描器帶入的進出場水平線 ────────────────────────────
    if (signalPrices && showSigLines) {
      const sp = signalPrices;
      if (sp.entry) candle.createPriceLine({
        price: sp.entry, color: 'rgba(46,125,50,0.9)', lineWidth: 1.5,
        lineStyle: 0, axisLabelVisible: true, title: '進場',
      });
      if (sp.t1) candle.createPriceLine({
        price: sp.t1, color: 'rgba(184,146,77,0.85)', lineWidth: 1,
        lineStyle: 1, axisLabelVisible: true, title: 'T1',
      });
      if (sp.t2) candle.createPriceLine({
        price: sp.t2, color: 'rgba(184,146,77,0.55)', lineWidth: 1,
        lineStyle: 2, axisLabelVisible: true, title: 'T2',
      });
      if (sp.sl) candle.createPriceLine({
        price: sp.sl, color: 'rgba(192,57,43,0.85)', lineWidth: 1.5,
        lineStyle: 1, axisLabelVisible: true, title: '停損',
      });
    }

    // ── MACD 進出場標記 + D/E 事件標記 ──────────────────────
    {
      const validDatesArr = lw.map(d => d.time).filter(Boolean).sort();
      const validDatesSet = new Set(validDatesArr);
      const cutoff14m = validDatesArr.length > 0 ? validDatesArr[0] : '2000-01-01';

      function nearestTradingDay(dateStr) {
        if (!dateStr) return null;
        if (validDatesSet.has(dateStr)) return dateStr;
        const parts = dateStr.split('-').map(Number);
        const dt = new Date(parts[0], parts[1] - 1, parts[2]);
        for (let i = 1; i <= 7; i++) {
          dt.setDate(dt.getDate() + 1);
          const s = dt.toISOString().slice(0, 10);
          if (validDatesSet.has(s)) return s;
        }
        return null;
      }

      const markers = [];

      // MACD 買賣箭頭
      for (let i = 1; i < lw.length; i++) {
        const d = lw[i], p = lw[i - 1];
        if (d.dif == null || d.dea == null || p.dif == null || p.dea == null) continue;
        if (p.dif < p.dea && d.dif >= d.dea) {
          markers.push({ time: d.time, position: 'belowBar', color: '#4caf50', shape: 'arrowUp',   text: '買', size: 1 });
        } else if (p.dif > p.dea && d.dif <= d.dea) {
          markers.push({ time: d.time, position: 'aboveBar', color: '#ef5350', shape: 'arrowDown', text: '賣', size: 1 });
        }
      }

      // D markers — 除息日
      if (Array.isArray(dividends) && dividends.length > 0) {
        dividends.forEach(div => {
          const raw = div.CashExDividendTradingDate || div.StockExDividendTradingDate || div.ex_dividend_date || '';
          if (!raw) return;
          const t = nearestTradingDay(raw);
          if (!t || t < cutoff14m) return;
          markers.push({ time: t, position: 'belowBar', color: '#b8924d', shape: 'circle', text: 'D', size: 1 });
        });
      }

      // E markers — 財報日
      if (Array.isArray(financials) && financials.length > 0) {
        financials.forEach(q => {
          const raw = q.date || '';
          if (!raw) return;
          const t = nearestTradingDay(raw);
          if (!t || t < cutoff14m) return;
          markers.push({ time: t, position: 'belowBar', color: '#2a6b7c', shape: 'square', text: 'E', size: 1 });
        });
      }

      markers.sort((a, b) => a.time < b.time ? -1 : a.time > b.time ? 1 : 0);
      if (markers.length) candle.setMarkers(markers);
    }

    // Volume overlay (bottom 20% of main pane)
    if (ind.volume) {
      const vol = main.addHistogramSeries({
        priceFormat:{ type:'volume' },
        priceScaleId:'vol',
        color: UP,
      });
      main.priceScale('vol').applyOptions({ scaleMargins:{ top:0.82, bottom:0 } });
      vol.setData(lw.map(d => ({
        time:d.time, value:d.volume,
        color: d.close >= d.open ? UP+'66' : DN+'66',
      })));
    }

    // EMA lines
    if (ind.ema) {
      [['ema20',TV.ema20,1],['ema50',TV.ema50,1.2],['ema100',TV.ema100,1.4],['ema200',TV.ema200,1.7]]
        .forEach(([k,color,w]) => {
          const s = main.addLineSeries({ color, lineWidth:w, priceLineVisible:false, lastValueVisible:false, crosshairMarkerVisible:false });
          s.setData(lw.filter(d=>d[k]!=null).map(d=>({ time:d.time, value:d[k] })));
        });
    }

    // SMA 均線（週/月/季/年）
    if (maOn) {
      MA_LINES.forEach(m => {
        const s = main.addLineSeries({ color: m.color, lineWidth: 1.2, priceLineVisible:false, lastValueVisible:false, crosshairMarkerVisible:false });
        s.setData(lw.filter(d => d[m.id] != null).map(d => ({ time: d.time, value: d[m.id] })));
      });
    }

    // Bollinger Bands
    if (ind.bb) {
      [['bbU',0],['bbM',2],['bbL',0]].forEach(([k,style]) => {
        const s = main.addLineSeries({ color:TV.bb, lineWidth:1, lineStyle:style, priceLineVisible:false, lastValueVisible:false, crosshairMarkerVisible:false });
        s.setData(lw.filter(d=>d[k]!=null).map(d=>({ time:d.time, value:d[k] })));
      });
    }

    // ── MACD ─────────────────────────────────────────────
    if (ind.macd && macdRef.current) {
      const mc = LC.createChart(macdRef.current, { ...base, height:150,
        timeScale:{ ...base.timeScale, timeVisible:!ind.rsi } });
      chartsRef.current.push(mc);

      const histo = mc.addHistogramSeries({ priceLineVisible:false, lastValueVisible:false });
      histo.setData(lw.filter(d=>d.hist!=null).map(d=>({
        time:d.time, value:d.hist,
        color: hist4Color(d.hist, d.prevHist),
      })));
      const difS = mc.addLineSeries({ color:'#4caf50', lineWidth:1.4, priceLineVisible:false, lastValueVisible:false });
      difS.setData(lw.filter(d=>d.dif!=null).map(d=>({ time:d.time, value:d.dif })));
      const deaS = mc.addLineSeries({ color:'#ef5350', lineWidth:1.1, priceLineVisible:false, lastValueVisible:false });
      deaS.setData(lw.filter(d=>d.dea!=null).map(d=>({ time:d.time, value:d.dea })));

      // 交叉點標記在 MACD 副圖
      const macdMarkers = [];
      for (let i = 1; i < lw.length; i++) {
        const d = lw[i], p = lw[i - 1];
        if (d.dif == null || d.dea == null || p.dif == null || p.dea == null) continue;
        if (p.dif < p.dea && d.dif >= d.dea) {
          macdMarkers.push({ time: d.time, position: 'belowBar', color: '#4caf50', shape: 'circle', text: '', size: 1 });
        } else if (p.dif > p.dea && d.dif <= d.dea) {
          macdMarkers.push({ time: d.time, position: 'aboveBar', color: '#ef5350', shape: 'circle', text: '', size: 1 });
        }
      }
      if (macdMarkers.length) difS.setMarkers(macdMarkers);
    }

    // ── RSI ───────────────────────────────────────────────
    if (ind.rsi && rsiRef.current) {
      const rc = LC.createChart(rsiRef.current, { ...base, height:130,
        timeScale:{ ...base.timeScale, timeVisible:true } });
      chartsRef.current.push(rc);

      const rsiS = rc.addLineSeries({ color:'#ab47bc', lineWidth:1.4, priceLineVisible:false, lastValueVisible:false });
      rsiS.setData(lw.filter(d=>d.rsi!=null).map(d=>({ time:d.time, value:d.rsi })));
      [[70,TV.bear,'2'],[50,TV.grid,'2'],[30,TV.bull,'2']].forEach(([v,c]) => {
        const s = rc.addLineSeries({ color:c, lineWidth:0.6, lineStyle:2, priceLineVisible:false, lastValueVisible:false, crosshairMarkerVisible:false });
        s.setData(lw.map(d=>({ time:d.time, value:v })));
      });
    }

    // ── 十字游標 OHLC 資訊卡(動效升級層 v1)──
    {
      const byTime = new Map(lw.map(d => [d.time, d]));
      main.subscribeCrosshairMove(param => {
        const el = ohlcRef.current;
        if (!el) return;
        const d = param && param.time != null ? byTime.get(param.time) : null;
        if (!d || !param.point) { el.style.opacity = '0'; return; }
        const i = lw.indexOf(d);
        const prevC = i > 0 ? lw[i - 1].close : d.open;
        const chg = d.close - prevC, pct = prevC ? chg / prevC * 100 : 0;
        const cc = chg >= 0 ? TV.bull : TV.bear;
        const f = x => x == null ? '—' : Number(x).toFixed(2);
        const volTxt = d.volume != null
          ? (d.volume >= 1000 ? Math.round(d.volume / 1000).toLocaleString('en-US') + ' 張' : d.volume + ' 股')
          : null;
        el.innerHTML =
          '<div style="color:#d1d4dc;font-weight:700;margin-bottom:3px;letter-spacing:.06em">' + d.time + '</div>' +
          '<div>開 <b style="color:#d1d4dc">' + f(d.open) + '</b>&nbsp; 高 <b style="color:' + TV.bull + '">' + f(d.high) + '</b></div>' +
          '<div>低 <b style="color:' + TV.bear + '">' + f(d.low) + '</b>&nbsp; 收 <b style="color:' + cc + '">' + f(d.close) + '</b></div>' +
          '<div style="color:' + cc + '">' + (chg >= 0 ? '▲ +' : '▼ ') + chg.toFixed(2) + ' (' + (chg >= 0 ? '+' : '') + pct.toFixed(2) + '%)</div>' +
          (volTxt ? '<div style="color:#787b86">量 ' + volTxt + '</div>' : '');
        const w = mainRef.current ? mainRef.current.clientWidth : 600;
        const flip = param.point.x > w - 180;
        el.style.left = flip ? (param.point.x - 168) + 'px' : (param.point.x + 16) + 'px';
        el.style.top  = Math.max(6, param.point.y - 24) + 'px';
        el.style.opacity = '1';
      });
    }

    // Sync time scales across all panes
    const all = chartsRef.current;
    all.forEach((src, si) => {
      src.timeScale().subscribeVisibleLogicalRangeChange(range => {
        if (!range) return;
        all.forEach((dst, di) => { if (di !== si) try { dst.timeScale().setVisibleLogicalRange(range); } catch(_){} });
      });
    });

    main.timeScale().fitContent();

    return () => {
      chartsRef.current.forEach(c => { try { c.remove(); } catch(_) {} });
      chartsRef.current = [];
    };
  }, [lw, ind, maOn, showSigLines, signalPrices]);

  // ── 測量模式:開關時鎖定/解鎖圖表拖移縮放 ──
  useEffect(() => {
    const main = chartsRef.current[0];
    if (main) try { main.applyOptions({ handleScroll: !measOn, handleScale: !measOn }); } catch(_) {}
    if (!measOn) {
      measDrag.current = null;
      if (measBoxRef.current) measBoxRef.current.style.display = 'none';
    }
  }, [measOn, lw]);

  function _measPt(e) {
    const r = measLayerRef.current.getBoundingClientRect();
    return { x: e.clientX - r.left, y: e.clientY - r.top };
  }
  function measDown(e) {
    e.preventDefault();
    try { measLayerRef.current.setPointerCapture(e.pointerId); } catch(_) {}
    measDrag.current = _measPt(e);
    measDraw(measDrag.current, measDrag.current);
  }
  function measMove(e) { if (measDrag.current) measDraw(measDrag.current, _measPt(e)); }
  function measUp() { measDrag.current = null; }
  function measDraw(a, b) {
    const box = measBoxRef.current, candle = candleRef.current, main = chartsRef.current[0];
    if (!box || !candle || !main) return;
    const x1 = Math.min(a.x, b.x), x2 = Math.max(a.x, b.x);
    const y1 = Math.min(a.y, b.y), y2 = Math.max(a.y, b.y);
    const p1 = candle.coordinateToPrice(a.y), p2 = candle.coordinateToPrice(b.y);
    let bars = 0;
    try {
      const l1 = main.timeScale().coordinateToLogical(a.x);
      const l2 = main.timeScale().coordinateToLogical(b.x);
      if (l1 != null && l2 != null) bars = Math.abs(Math.round(l2 - l1));
    } catch(_) {}
    const diff = (p2 ?? 0) - (p1 ?? 0);
    const pct  = p1 ? diff / p1 * 100 : 0;
    const up   = diff >= 0;
    const col  = up ? TV.bull : TV.bear;
    box.style.display = 'block';
    box.style.left = x1 + 'px';
    box.style.top  = y1 + 'px';
    box.style.width  = Math.max(2, x2 - x1) + 'px';
    box.style.height = Math.max(2, y2 - y1) + 'px';
    box.style.borderColor = col;
    box.style.background  = up ? 'rgba(42,107,76,.10)' : 'rgba(156,43,43,.10)';
    const lab = box.firstChild;
    lab.style.color = col;
    // 太貼近頂緣時資訊卡翻到框下方
    if (y1 < 60) { lab.style.top = '100%'; lab.style.bottom = 'auto'; lab.style.marginTop = '4px'; }
    else         { lab.style.bottom = '100%'; lab.style.top = 'auto'; lab.style.marginBottom = '4px'; }
    lab.innerHTML =
      '<b>' + (up ? '+' : '') + diff.toFixed(2) + '</b> (' + (up ? '+' : '') + pct.toFixed(2) + '%)' +
      '<span style="color:#787b86">&nbsp;·&nbsp;' + bars + ' 根</span>' +
      (p1 != null && p2 != null
        ? '<div style="color:#787b86">' + p1.toFixed(2) + ' → ' + p2.toFixed(2) + '</div>'
        : '');
  }

  // Stats for toolbar
  const last  = vis[vis.length - 1] || {};
  const first = vis[0] || {};
  const tfChg = (last.close||0) - (first.close||0);
  const tfPct = first.close > 0 ? (tfChg/first.close)*100 : 0;

  // Current EMA values
  const emaVals = lw.length ? lw[lw.length-1] : {};

  return (
    <div className="chartpro-wrap">

      {/* Toolbar */}
      <div className="cp-toolbar">
        <div className="cp-symbol">
          <span className="num">{stock?.code}</span>
          <span className="kai">{stock?.name}</span>
          <span className="cp-exchange italic-en">{stock?.exchange || 'TWSE'} · 1D</span>
        </div>
        <div className="cp-tf">
          {rangeTabs
            ? rangeTabs.tabs.map((r, i) => (
                <button key={i} className={`cp-tf-btn ${rangeTabs.activeIdx===i?'on':''}`}
                  onClick={() => rangeTabs.onSelect(i)}>
                  {r.label}
                </button>
              ))
            : TF_PRESETS.map(p => (
                <button key={p.id} className={`cp-tf-btn ${tf===p.id?'on':''}`} onClick={()=>setTf(p.id)}>
                  {p.label}
                </button>
              ))
          }
        </div>
        <span className="cp-ind-tfchg">
          <span className={`num ${tfChg>=0?'bull-c':'bear-c'}`}>
            <b>{tfChg>=0?'+':''}{tfChg.toFixed(2)}</b>&nbsp;({tfPct>=0?'+':''}{tfPct.toFixed(2)}%)
          </span>
        </span>
        <button className={`cp-tf-btn ${measOn?'on':''}`}
                onClick={()=>setMeasOn(v=>!v)}
                title="拖曳測量區間漲跌">
          ⊿ 測量
        </button>
      </div>

      {/* Indicator chips */}
      <div className="cp-ind-row">
        <button className={`cp-ind-chip ${maOn?'on':''}`} onClick={()=>setMaOn(v=>!v)}>
          <span className="chip-mark">{maOn?'◉':'○'}</span>
          <span className="chip-name">MA</span>
          <span className="chip-cn kai">週月季年均線</span>
        </button>
        <span style={{width:1,background:'#2a2e39',margin:'0 4px',alignSelf:'stretch'}}/>
        {INDS.map(i => (
          <button key={i.id}
                  className={`cp-ind-chip ${ind[i.id]?'on':''}`}
                  onClick={()=>setInd(prev=>({...prev,[i.id]:!prev[i.id]}))}>
            <span className="chip-mark">{ind[i.id]?'◉':'○'}</span>
            <span className="chip-name">{i.label}</span>
            <span className="chip-cn kai">{i.cn}</span>
          </button>
        ))}
        {signalPrices && (
          <>
            <span style={{width:1,background:'#2a2e39',margin:'0 4px',alignSelf:'stretch'}}/>
            <button className={`cp-ind-chip ${showSigLines?'on':''}`}
                    onClick={()=>setShowSigLines(v=>!v)}>
              <span className="chip-mark">{showSigLines?'◉':'○'}</span>
              <span className="chip-name">Lines</span>
              <span className="chip-cn kai">進出場線</span>
            </button>
          </>
        )}
      </div>

      {/* EMA Legend */}
      {ind.ema && (
        <div className="cp-legend" style={{ padding:'4px 12px', gap:14 }}>
          <span style={{ fontFamily:'Times New Roman,serif', fontStyle:'italic', fontSize:12.5, color:TV.text }}>
            EMA 20/50/100/200
          </span>
          {[['ema20',TV.ema20,'20'],['ema50',TV.ema50,'50'],['ema100',TV.ema100,'100'],['ema200',TV.ema200,'200']]
            .map(([k,c,n]) => (
              <span key={k} style={{ color:c, fontSize:13 }}>
                <b>{n}</b>&nbsp;<span className="num">{emaVals[k]?.toFixed(1)??'—'}</span>
              </span>
          ))}
        </div>
      )}

      {/* Chart containers */}
      <div style={{ position:'relative' }}>
        <div ref={mainRef} style={{ width:'100%' }}/>
        <div ref={ohlcRef} style={{
          position:'absolute', top:6, left:6, zIndex:5, pointerEvents:'none',
          minWidth:130, padding:'8px 12px', opacity:0,
          background:'rgba(19,23,34,.93)', border:'1px solid #2a2e39',
          borderLeft:'2px solid #b8924d',
          fontFamily:'Times New Roman,serif', fontSize:13, lineHeight:1.7,
          color:'#787b86', transition:'opacity .12s ease',
          boxShadow:'0 6px 20px rgba(0,0,0,.4)',
        }}/>
        {/* 測量工具覆蓋層:開啟時攔截拖曳 */}
        <div ref={measLayerRef}
             onPointerDown={measDown} onPointerMove={measMove}
             onPointerUp={measUp} onPointerCancel={measUp}
             style={{ position:'absolute', inset:0, zIndex:6,
                      cursor:'crosshair', touchAction:'none',
                      display: measOn ? 'block' : 'none' }}>
          <div ref={measBoxRef} style={{
            position:'absolute', display:'none',
            border:'1px dashed', pointerEvents:'none',
          }}>
            <div style={{
              position:'absolute', bottom:'100%', left:0, marginBottom:4,
              whiteSpace:'nowrap',
              background:'rgba(19,23,34,.93)', border:'1px solid #2a2e39',
              borderLeft:'2px solid #b8924d',
              padding:'5px 9px',
              fontFamily:'Times New Roman,serif', fontSize:13, lineHeight:1.5,
            }}/>
          </div>
        </div>
      </div>
      {ind.macd && <div ref={macdRef} style={{ width:'100%', marginTop:2 }}/>}
      {ind.rsi  && <div ref={rsiRef}  style={{ width:'100%', marginTop:2 }}/>}

    </div>
  );
}

window.ChartPro = ChartPro;
