// US Dashboard components — extracted from 美股總覽.html
const { useState, useEffect, useMemo, useRef, useCallback } = React;

function fmtN(n, d=2) {
  if (n == null || isNaN(n)) return '—';
  return n.toLocaleString('en-US', { minimumFractionDigits:d, maximumFractionDigits:d });
}
function fmtVol(v) {
  if (!v) return '—';
  if (v >= 1e9) return (v/1e9).toFixed(1)+'B';
  if (v >= 1e6) return (v/1e6).toFixed(1)+'M';
  return String(v);
}
function zh(ticker) { return (window.US_ZH || {})[ticker] || ''; }

function USTicker({ indices, macro }) {
  const items = useMemo(() => {
    const base = (indices || []).map(m => ({
      name: m.name, val: fmtN(m.price), chg: m.chg, pos: m.chg >= 0
    }));
    if (macro) {
      if (macro.gold)   base.push({ name:'黃金',    val: fmtN(macro.gold.price,0),   chg: macro.gold.chgPct,   pos: macro.gold.chgPct   >= 0 });
      if (macro.btc)    base.push({ name:'比特幣',  val: fmtN(macro.btc.price,0),    chg: macro.btc.chgPct,    pos: macro.btc.chgPct    >= 0 });
      if (macro.oil)    base.push({ name:'WTI原油', val: fmtN(macro.oil.price),      chg: macro.oil.chgPct,    pos: macro.oil.chgPct    >= 0 });
      if (macro.usdtwd) base.push({ name:'美元/台幣',val:fmtN(macro.usdtwd.price),   chg: macro.usdtwd.chgPct, pos: macro.usdtwd.chgPct >= 0 });
    }
    return [...base, ...base];
  }, [indices, macro]);
  return (
    <div className="ticker">
      <div className="ticker-track">
        {items.map((m, i) => (
          <span key={i} className="ticker-item">
            <span className="name kai">{m.name}</span>
            <span className="val">{m.val}</span>
            <span className={`chg ${m.pos ? 'up' : 'dn'}`}>{m.pos ? '+' : ''}{fmtN(m.chg)}%</span>
          </span>
        ))}
      </div>
    </div>
  );
}

function IdxChart({ hist, price, chg, pct, label }) {
  const W = 320, H = 130, P = 22;
  const data = (hist || []).filter(d => d.close > 0);
  if (data.length < 2) return (
    <div className="idx-chart">
      <div style={{display:'flex',alignItems:'center',justifyContent:'center',height:H,color:'var(--ink-mute)',fontStyle:'italic',fontSize:12}}>載入中…</div>
    </div>
  );
  const vals = data.map(d => d.close);
  const min = Math.min(...vals), max = Math.max(...vals), rng = max - min || 1;
  const tx = i => P + (i/(vals.length-1))*(W-P*2);
  const ty = v => H-P - ((v-min)/rng)*(H-P*2);
  const path = vals.map((v,i) => (i?'L':'M')+tx(i).toFixed(1)+' '+ty(v).toFixed(1)).join(' ');
  const area = path + ' L '+(W-P)+' '+(H-P)+' L '+P+' '+(H-P)+' Z';
  const col = chg >= 0 ? '#2a6b4c' : '#9c2b2b';
  const axI = [0, Math.floor(data.length/2), data.length-1];
  return (
    <div className="idx-chart">
      <div className="idx-chart-head">
        <div>
          <div className="idx-chart-label" style={{fontStyle:'italic'}}>{label}</div>
          <div className="idx-chart-value num">{fmtN(price)}</div>
          <div className={`idx-chart-chg num ${chg>=0?'bull-c':'bear-c'}`}>{chg>=0?'+':''}{fmtN(chg)} ({pct>=0?'+':''}{fmtN(pct)}%)</div>
        </div>
        <div className="idx-range">
          <div><span>90D High</span><b className="num">{fmtN(max)}</b></div>
          <div><span>90D Low</span><b className="num">{fmtN(min)}</b></div>
        </div>
      </div>
      <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" style={{width:'100%',height:H,marginTop:8}}>
        <defs>
          <linearGradient id={`ug${label.slice(0,3)}`} x1="0" x2="0" y1="0" y2="1">
            <stop offset="0%" stopColor={col} stopOpacity=".2"/>
            <stop offset="100%" stopColor={col} stopOpacity="0"/>
          </linearGradient>
        </defs>
        {[0,1,2,3].map(i=>(
          <line key={i} x1={P} x2={W-P} y1={P+(i/3)*(H-P*2)} y2={P+(i/3)*(H-P*2)} stroke="var(--paper-line)" strokeWidth=".6" strokeDasharray="2 4"/>
        ))}
        <path d={area} fill={`url(#ug${label.slice(0,3)})`}/>
        <path d={path} fill="none" stroke={col} strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round"/>
        {axI.map(i => data[i] ? (
          <text key={i} x={tx(i)} y={H-5} textAnchor="middle" fontFamily="Times New Roman" fontStyle="italic" fontSize="8" fill="var(--ink-mute)">{data[i].date}</text>
        ) : null)}
      </svg>
    </div>
  );
}

const SECTOR_SIZE = { XLK:'XL', XLF:'L', XLV:'L', XLY:'M', XLI:'M', XLC:'M', XLE:'M', XLP:'S', XLU:'S', XLB:'S', XLRE:'S', SOXX:'M' };
const SECTOR_SPAN = { XL:'span 2 / span 2', L:'span 2 / span 1', M:'span 1 / span 1', S:'span 1 / span 1' };
function sCol(chg) {
  if (chg >= 3)   return '#1a6b35';
  if (chg >= 1.5) return '#2e8c4a';
  if (chg >= 0.5) return '#5a9e6e';
  if (chg >= 0)   return '#8aac94';
  if (chg >=-0.5) return '#ac8a8a';
  if (chg >=-1.5) return '#9e4040';
  return '#7a1a1a';
}
function USSectorHeat({ sectors }) {
  if (!sectors?.length) return <div className="loading-box">Loading sectors…</div>;
  return (
    <div className="heat-grid-us">
      {sectors.map((s, i) => {
        const size = SECTOR_SIZE[s.etf] || 'S';
        return (
          <a key={i} href={`美股個股.html?code=${s.etf}`} className="heat-cell-us" style={{background:sCol(s.chg), gridArea:SECTOR_SPAN[size]}}>
            <div className="hc-name">{s.name}</div>
            <div className="hc-en">{s.en}</div>
            <div className="hc-etf">{s.etf}</div>
            <div className="hc-chg">{s.chg>=0?'+':''}{fmtN(s.chg)}%</div>
          </a>
        );
      })}
    </div>
  );
}

const MACRO_DEFS = [
  { key:'vix',    name:'恐慌指數',  en:'VIX',         fmt: v => fmtN(v,2),   note:'< 20 偏低',    invert: true },
  { key:'gold',   name:'黃金',      en:'Gold ($/oz)', fmt: v => '$'+fmtN(v,0),note:'避險需求',    invert: false },
  { key:'oil',    name:'WTI 原油',  en:'Crude Oil',   fmt: v => '$'+fmtN(v,2),note:'通膨代理指標', invert: false },
  { key:'btc',    name:'比特幣',    en:'Bitcoin',     fmt: v => '$'+fmtN(v,0),note:'風險情緒',    invert: false },
  { key:'usdtwd', name:'美元/台幣', en:'USD / TWD',   fmt: v => fmtN(v,2),   note:'匯率參考',    invert: false },
];
function MacroBackdrop({ macro }) {
  return (
    <div className="macro-strip">
      {MACRO_DEFS.map(def => {
        const d = macro?.[def.key];
        const cls = !d ? 'neu' : (d.chgPct >= 0 ? (def.invert?'dn':'up') : (def.invert?'up':'dn'));
        return (
          <div key={def.key} className={`macro-cell ${cls}`}>
            <div className="macro-lbl">{def.en}</div>
            <div className="macro-name">{def.name}</div>
            <div className="macro-val num">{d ? def.fmt(d.price) : <span className="skel" style={{display:'inline-block',width:60,height:16}}/>}</div>
            {d && <div className={`macro-chg ${d.chgPct>=0?'up':'dn'}`}>{d.chgPct>=0?'+':''}{fmtN(d.chgPct)}%&nbsp;<span style={{color:'var(--ink-mute)',fontSize:'var(--fs-nano)'}}>{def.note}</span></div>}
          </div>
        );
      })}
    </div>
  );
}

function FearGreed({ vixPrice }) {
  const score = useMemo(() => {
    if (!vixPrice) return 50;
    if (vixPrice < 12) return 88;
    if (vixPrice < 17) return 70 - (vixPrice-12)*4;
    if (vixPrice < 20) return 50 - (vixPrice-17)*5;
    if (vixPrice < 25) return 35 - (vixPrice-20)*3;
    if (vixPrice < 30) return 20 - (vixPrice-25)*2;
    return Math.max(5, 10-(vixPrice-30));
  }, [vixPrice]);
  const label = score >= 75 ? '極度貪婪' : score >= 55 ? '貪婪' : score >= 45 ? '中性' : score >= 25 ? '恐懼' : '極度恐懼';
  const col   = score >= 60 ? 'var(--bull)' : score >= 40 ? 'var(--gold)' : 'var(--bear)';
  const R = 54, cx = 70, cy = 70;
  const a = Math.PI - (score/100)*Math.PI;
  const nx = cx + R*Math.cos(a), ny = cy + R*Math.sin(a);
  return (
    <div style={{border:'1px solid var(--paper-line)',background:'var(--paper-bright)',padding:'16px 20px',display:'flex',flexDirection:'column',alignItems:'center'}}>
      <div className="eyebrow" style={{marginBottom:8,alignSelf:'flex-start'}}>Fear &amp; Greed Index</div>
      <svg viewBox="0 0 140 80" style={{width:'100%',maxWidth:200,height:'auto'}}>
        {[0,25,50,75,100].map((t,i) => {
          const ang = Math.PI - (t/100)*Math.PI;
          const r1=44,r2=64;
          return <line key={i} x1={cx+r1*Math.cos(ang)} y1={cy+r1*Math.sin(ang)} x2={cx+r2*Math.cos(ang)} y2={cy+r2*Math.sin(ang)} stroke="var(--paper-line)" strokeWidth=".8"/>;
        })}
        <path d={`M ${cx-R} ${cy} A ${R} ${R} 0 0 1 ${cx+R} ${cy}`} fill="none" stroke="var(--paper-line)" strokeWidth="12"/>
        <path d={`M ${cx-R} ${cy} A ${R} ${R} 0 0 1 ${cx+R*Math.cos(Math.PI-(score/100)*Math.PI)} ${cy+R*Math.sin(Math.PI-(score/100)*Math.PI)}`} fill="none" stroke={col} strokeWidth="12" strokeLinecap="round"/>
        <line x1={cx} y1={cy} x2={nx} y2={ny} stroke="var(--ink)" strokeWidth="1.8" strokeLinecap="round"/>
        <circle cx={cx} cy={cy} r="4" fill="var(--ink)"/>
        <text x={cx-R-4} y={cy+16} textAnchor="middle" fontFamily="Times New Roman" fontSize="7" fill="var(--bear)">恐懼</text>
        <text x={cx+R+4} y={cy+16} textAnchor="middle" fontFamily="Times New Roman" fontSize="7" fill="var(--bull)">貪婪</text>
        <text x={cx} y={cy-8} textAnchor="middle" fontFamily="Times New Roman" fontWeight="700" fontSize="22" fill={col}>{score}</text>
      </svg>
      <div style={{fontFamily:"'BiauKai','標楷體',serif",fontSize:15,fontWeight:700,letterSpacing:'.14em',color:col,marginTop:4}}>{label}</div>
      <div style={{fontSize:'var(--fs-nano)',color:'var(--ink-mute)',fontStyle:'italic',marginTop:3}}>以 VIX 指數推算</div>
    </div>
  );
}

function MarketBreadth({ sectors }) {
  const up  = (sectors||[]).filter(s => s.chg > 0).length;
  const dn  = (sectors||[]).filter(s => s.chg < 0).length;
  const tot = up + dn || 1;
  const ADV = 2148, DCL = 1234, UNC = 312, totalStocks = ADV+DCL+UNC;
  return (
    <div style={{border:'1px solid var(--paper-line)',background:'var(--paper-bright)',padding:'16px 20px'}}>
      <div className="eyebrow" style={{marginBottom:10}}>Market Breadth · 市場廣度</div>
      <div style={{fontSize:'var(--fs-nano)',color:'var(--ink-mute)',marginBottom:4,fontStyle:'italic'}}>GICS Sectors — {up} 上漲 / {dn} 下跌</div>
      <div className="breadth-bar">
        <div className="breadth-up" style={{width:Math.round(up/tot*100)+'%'}}/>
        <div className="breadth-dn" style={{width:(100-Math.round(up/tot*100))+'%'}}/>
      </div>
      <div style={{fontSize:'var(--fs-nano)',color:'var(--ink-mute)',marginBottom:4,marginTop:12,fontStyle:'italic'}}>NYSE 全市場 — 參考數據</div>
      <div className="breadth-bar">
        <div className="breadth-up"   style={{width:(ADV/totalStocks*100)+'%'}}/>
        <div className="breadth-flat" style={{width:(UNC/totalStocks*100)+'%'}}/>
        <div className="breadth-dn"   style={{width:(DCL/totalStocks*100)+'%'}}/>
      </div>
      <div className="breadth-stats">
        <div className="bstat"><div className="lbl">上漲</div><div className="val up">{ADV.toLocaleString()}</div></div>
        <div className="bstat"><div className="lbl">下跌</div><div className="val dn">{DCL.toLocaleString()}</div></div>
        <div className="bstat"><div className="lbl">52週新高</div><div className="val">{148}</div></div>
        <div className="bstat"><div className="lbl">52週新低</div><div className="val dn">{38}</div></div>
      </div>
    </div>
  );
}

const HOT_THEMES = [
  { en:'AI Compute',       name:'AI 算力',    heat:'hot',  tickers:['NVDA','AMD','AVGO'],      pct:+18.4 },
  { en:'Semiconductors',   name:'半導體設備',  heat:'hot',  tickers:['AMAT','KLAC','LRCX'],     pct:+12.6 },
  { en:'GLP-1 / Obesity',  name:'GLP-1 減重', heat:'hot',  tickers:['LLY','NVO'],              pct:+22.8 },
  { en:'Quantum Computing', name:'量子運算',   heat:'warm', tickers:['IONQ','IBM','MSFT'],      pct: +8.2 },
  { en:'Crypto Assets',    name:'加密資產',    heat:'warm', tickers:['COIN','MSTR','HOOD'],     pct: +6.8 },
  { en:'Nuclear Energy',   name:'核能復興',    heat:'warm', tickers:['CEG','VST','CCJ'],        pct: +9.4 },
  { en:'Defense & Space',  name:'國防航太',    heat:'cool', tickers:['LMT','RTX','NOC','AXON'], pct: +4.2 },
  { en:'Digital Finance',  name:'數位金融',    heat:'cool', tickers:['V','MA','SQ','PYPL'],     pct: +3.8 },
];
function HotThemes() {
  const maxPct = Math.max(...HOT_THEMES.map(t => t.pct));
  return (
    <div className="themes-grid">
      {HOT_THEMES.map((t, i) => (
        <div key={i} className={`theme-card ${t.heat}`}>
          <div className="theme-en">{t.en}</div>
          <div className="theme-name">{t.name}</div>
          <div className="theme-tickers">{t.tickers.join(' · ')}</div>
          <div className="theme-mom">
            <div className="mom-bar"><div className={`mom-fill ${t.heat}`} style={{width:(t.pct/maxPct*100)+'%'}}/></div>
            <div className={`theme-pct ${t.pct>=0?'up':'dn'}`}>{t.pct>=0?'+':''}{t.pct.toFixed(1)}%</div>
          </div>
        </div>
      ))}
    </div>
  );
}

const EARN_WATCHLIST = ['NVDA','AAPL','MSFT','GOOGL','META','AMZN','TSLA','TSM','AMD','AVGO'];
function fmtRevenue(v) { if (v==null||isNaN(v)) return '—'; if (Math.abs(v)>=1e9) return '$'+(v/1e9).toFixed(1)+'B'; if (Math.abs(v)>=1e6) return '$'+(v/1e6).toFixed(0)+'M'; return '$'+v.toLocaleString('en-US'); }
function fmtEps(v)     { if (v==null||isNaN(v)) return '—'; return (v>=0?'$':'-$')+Math.abs(v).toFixed(2); }
function fmtDateMMDD(s) { if (!s) return '—'; const d=new Date(s+'T12:00:00'); return (d.getMonth()+1).toString().padStart(2,'0')+'/'+d.getDate().toString().padStart(2,'0'); }
function daysUntil(s)  { if (!s) return null; const t=new Date(); t.setHours(0,0,0,0); const d=new Date(s+'T12:00:00'); d.setHours(0,0,0,0); return Math.round((d-t)/86400000); }

function EarningsCalendar() {
  const [rows, setRows] = useState(null);
  useEffect(() => {
    const today=new Date(); today.setHours(0,0,0,0);
    const past14=new Date(today); past14.setDate(past14.getDate()-14);
    const future30=new Date(today); future30.setDate(future30.getDate()+30);
    Promise.allSettled(EARN_WATCHLIST.map(sym =>
      fetch(`/api/stock?type=fmp_earnings&symbol=${sym}`).then(r=>r.ok?r.json():null).then(d=>({sym,data:d?.data||[]})).catch(()=>({sym,data:[]}))
    )).then(results => {
      const collected=[];
      results.forEach(res => {
        if (res.status!=='fulfilled') return;
        const {sym,data}=res.value;
        if (!data.length) return;
        for (const entry of data) {
          const d=new Date(entry.date+'T12:00:00'); d.setHours(0,0,0,0);
          const isUpcoming=d>=today&&d<=future30, isRecent=d>=past14&&d<today;
          if (isUpcoming||isRecent) { collected.push({sym,...entry,isUpcoming,isRecent}); break; }
        }
        if (!collected.find(r=>r.sym===sym)&&data.length>0) {
          const latest=data[0]; const ld=new Date(latest.date+'T12:00:00'); ld.setHours(0,0,0,0);
          if (ld<today) collected.push({sym,...latest,isUpcoming:false,isRecent:false});
        }
      });
      collected.sort((a,b)=>{
        if (a.isUpcoming&&!b.isUpcoming) return -1; if (!a.isUpcoming&&b.isUpcoming) return 1;
        if (a.isUpcoming&&b.isUpcoming)  return new Date(a.date)-new Date(b.date);
        return new Date(b.date)-new Date(a.date);
      });
      setRows(collected);
    });
  }, []);
  if (rows===null) return <div className="earn-loading">載入財報資料中…</div>;
  if (!rows.length) return <div className="earn-loading">近期無財報資料</div>;
  const upcoming=rows.filter(r=>r.isUpcoming), reported=rows.filter(r=>!r.isUpcoming);
  function renderRows(list) {
    return list.map((r,i)=>{
      const days=daysUntil(r.date), hasActual=r.epsActual!=null&&!isNaN(r.epsActual), hasEst=r.epsEstimated!=null&&!isNaN(r.epsEstimated)&&r.epsEstimated!==0;
      const surprise=(hasActual&&hasEst)?(r.epsActual-r.epsEstimated)/Math.abs(r.epsEstimated)*100:null;
      const beatMiss=surprise!=null?(surprise>=0?'beat':'miss'):null, zhName=zh(r.sym);
      return (
        <tr key={r.sym+i}>
          <td><a href={`美股個股.html?code=${r.sym}`} style={{textDecoration:'none',color:'inherit'}}><div className="earn-ticker-cell"><div className="et-code">{r.sym}</div>{zhName&&<div className="et-zh">{zhName}</div>}</div></a></td>
          <td className="earn-date-cell">
            <div>{fmtDateMMDD(r.date)}</div>
            {r.isUpcoming&&days!=null&&<div className="ed-countdown">{days===0?'今日':days===1?'明日':`${days} 天後`}</div>}
            {!r.isUpcoming&&<div className="ed-recent">{days!=null&&days<0?`${Math.abs(days)} 天前`:''}</div>}
          </td>
          <td className={`earn-num${!hasEst?' na':''}`}>{r.isUpcoming?(hasEst?fmtEps(r.epsEstimated):'—'):(hasActual?fmtEps(r.epsActual):'—')}</td>
          <td className={`earn-num hide-sm${r.revenueEstimated==null&&r.revenueActual==null?' na':''}`}>{r.isUpcoming?fmtRevenue(r.revenueEstimated):fmtRevenue(r.revenueActual)}</td>
          <td className="earn-surprise">
            {r.isUpcoming?(hasEst?<span className="es-pct upcoming">預估 {fmtEps(r.epsEstimated)}</span>:<span className="es-pct upcoming">—</span>):
              (surprise!=null?<div><div className={`es-pct ${beatMiss}`}>{surprise>=0?'+':''}{surprise.toFixed(1)}%</div><div className={`es-badge ${beatMiss}`}>{beatMiss==='beat'?'▲ beat':'▼ miss'}</div></div>:'—')}
          </td>
        </tr>
      );
    });
  }
  return (
    <div className="earn-wrap">
      <table className="earn-table">
        <thead><tr><th>代號</th><th>財報日</th><th className="num">EPS</th><th className="num hide-sm">營收</th><th className="num">驚喜 / 預估</th></tr></thead>
        <tbody>
          {upcoming.length>0&&<tr><td colSpan={5} className="earn-section-label">即將公布 — Upcoming Earnings</td></tr>}
          {renderRows(upcoming)}
          {reported.length>0&&<tr><td colSpan={5} className="earn-section-label">近期財報 — Recent Results</td></tr>}
          {renderRows(reported)}
        </tbody>
      </table>
    </div>
  );
}

const PRE_IPO = [
  { name:'OpenAI',    zh:'開放 AI',      sector:'AI · 人工智慧',       val:'$300B+', valSub:'最新估值',    cls:'ipo-expected', badge:'預期上市',    badgeCol:'var(--gold-deep)', year:'2025–2026', desc:'ChatGPT 開發商，全球 AI 浪潮旗手。Sam Altman 主導下獲微軟、軟銀超千億美元注資，已成全球估值最高 AI 企業。', note:'IPO 估值或突破 5000 億美元，遠超 Arm 上市規模', heat:5, backers:['Microsoft','SoftBank','Apple','a16z'] },
  { name:'SpaceX',    zh:'太空探索',     sector:'Space · 航太',         val:'$350B+', valSub:'最新估值',    cls:'ipo-partial',  badge:'Starlink 分拆', badgeCol:'var(--mid-tint)',  year:'2025–2026', desc:'商業航太絕對龍頭，Falcon 9 獨佔全球軌道發射逾 60%。Starlink 覆蓋 100+ 國家，年收入超 80 億美元。',     note:'Starlink 網路子公司可能率先獨立上市',         heat:5, backers:['Google','Fidelity','Founders Fund','Baillie Gifford'] },
  { name:'Anthropic', zh:'Anthropic',   sector:'AI Safety · AI安全',   val:'$61B+',  valSub:'2024 融資估值', cls:'ipo-watch',   badge:'持續觀察',    badgeCol:'var(--ink-mute)', year:'2026+',      desc:'Claude AI 開發商，由 OpenAI 前安全主管 Dario Amodei 創辦。以「憲法 AI」為核心，Amazon 已投資逾 40 億美元。', note:'Amazon + Google 合計投資超 80 億，深度綁定雲端生態', heat:4, backers:['Amazon','Google','Spark Capital','Salesforce'] },
  { name:'Stripe',    zh:'Stripe 支付', sector:'Fintech · 支付',        val:'$70B+',  valSub:'二手市場估值', cls:'ipo-expected', badge:'預期上市',    badgeCol:'var(--gold-deep)', year:'2025',       desc:'全球最大私有支付平台，年處理超 1 兆美元交易，服務 Amazon、Google 等 100 萬+ 企業客戶。',                  note:'已在多個私人交易平台活躍流通，IPO 意向明確',   heat:4, backers:['Sequoia','Andreessen Horowitz','Thrive Capital'] },
  { name:'Databricks', zh:'資料磚塊',   sector:'Data AI · 資料平台',    val:'$62B+',  valSub:'2024 融資估值', cls:'ipo-watch',   badge:'持續觀察',    badgeCol:'var(--ink-mute)', year:'2025–2026',  desc:'Data + AI 統一平台領導者，整合 Apache Spark 與 LLM 訓練基礎設施，ARR 破 20 億美元。',                     note:'2024 年底完成 100 億美元融資，NVIDIA 領投',    heat:4, backers:['NVIDIA','Google','Microsoft','Franklin Templeton'] },
  { name:'xAI',       zh:'xAI',         sector:'AI · 人工智慧',        val:'$50B+',  valSub:'2024 估值',    cls:'ipo-watch',   badge:'持續觀察',    badgeCol:'var(--ink-mute)', year:'2026+',      desc:'Elon Musk 創辦的 AI 公司，開發 Grok 模型，深度整合 X 逾 5 億用戶行為資料。',                              note:'已融資 120 億美元，可能合併至 X Corp 整體上市', heat:3, backers:['a16z','Founders Fund','Sequoia','Vy Capital'] },
  { name:'Klarna',    zh:'Klarna',      sector:'BNPL · 先買後付',       val:'$15B+',  valSub:'2025 估值',    cls:'ipo-filing',  badge:'已申報 SEC',  badgeCol:'var(--bull)',      year:'2025',       desc:'北歐最大先買後付平台，業務遍及 45 國，AI 驅動客服策略削減 85% 人工成本，盈利能力大幅提升。',                note:'已向 SEC 提交 S-1 文件，為 2025 年最受關注 IPO', heat:4, backers:['Sequoia','SoftBank','Silver Lake','GIC'] },
  { name:'Chime',     zh:'Chime 數位銀行', sector:'Neobank · 數位銀行', val:'$25B+',  valSub:'峰值估值',     cls:'ipo-expected', badge:'預期上市',    badgeCol:'var(--gold-deep)', year:'2025',       desc:'美國最大數位銀行，逾 2200 萬用戶，無隱藏費用模式顛覆傳統零售銀行，早領薪水功能深受低收入群體青睞。',       note:'曾估值 250 億美元，IPO 計劃因市場波動數度延後', heat:3, backers:['Sequoia','DST Global','Tiger Global','Coatue'] },
];
const IPO_STATUS_COL = { 'ipo-filing':'var(--bull)', 'ipo-expected':'var(--gold-deep)', 'ipo-partial':'var(--mid-tint)', 'ipo-watch':'var(--ink-mute)' };
function PreIPOWatchlist() {
  return (
    <div style={{marginBottom:28}}>
      <div className="ipo-section-header">
        <div>
          <div className="ipo-section-en">Pre-IPO Watchlist · Exclusive Coverage</div>
          <div className="ipo-section-cn">準備上市觀察名單</div>
          <div className="ipo-section-sub">估值最受矚目、即將登上華爾街的私人企業</div>
        </div>
        <div className="ipo-section-badge">8 Companies · 2025–2026</div>
      </div>
      <div className="ipo-grid">
        {PRE_IPO.map((co, i) => (
          <div key={i} className={`ipo-card ${co.cls}`}>
            <div className="ipo-top">
              <div>
                <div className="ipo-sector">{co.sector}</div>
                <div className="ipo-name">{co.name}</div>
                {co.zh !== co.name && <div className="ipo-zh">{co.zh}</div>}
              </div>
              <div className="ipo-val">{co.val}<small>{co.valSub}</small></div>
            </div>
            <div className="ipo-desc">{co.desc}</div>
            <div className="ipo-status-row">
              <span className="ipo-badge" style={{color:co.badgeCol}}>{co.badge}</span>
              <span className="ipo-year">{co.year}</span>
              <span className="ipo-heat">{'★'.repeat(co.heat)}{'☆'.repeat(5-co.heat)}</span>
            </div>
            <div className="ipo-note">{co.note}</div>
            <div className="ipo-backers">{co.backers.map((b,j)=><span key={j} className="ipo-backer">{b}</span>)}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

function USMovers({ gainers, losers }) {
  const MRow = ({ s }) => (
    <tr>
      <td><a href={`美股個股.html?code=${s.code}`} style={{textDecoration:'none',color:'inherit'}}><span className="mover-code">{s.code}</span>{zh(s.code)&&<span className="mover-zh">{zh(s.code)}</span>}<div className="mover-name">{s.name}</div></a></td>
      <td className="num" style={{textAlign:'right'}}>${fmtN(s.price)}</td>
      <td className={`mover-chg num ${s.pct>=0?'up':'dn'}`} style={{textAlign:'right'}}>{s.pct>=0?'+':''}{fmtN(s.pct)}%</td>
      <td style={{textAlign:'right',color:'var(--ink-mute)',fontSize:'var(--fs-nano)'}}>{fmtVol(s.vol)}</td>
    </tr>
  );
  const loading = !gainers?.length;
  return (
    <div className="movers-grid">
      {[{label:'漲幅榜 Top Gainers',data:gainers},{label:'跌幅榜 Top Losers',data:losers}].map(({label,data},gi)=>(
        <div key={gi}>
          <div className="eyebrow" style={{marginBottom:10}}>{label}</div>
          {loading?<div className="loading-box">Loading…</div>:
            <table className="movers-table">
              <thead><tr><th>股票</th><th style={{textAlign:'right'}}>Price</th><th style={{textAlign:'right'}}>漲跌%</th><th style={{textAlign:'right'}}>成交量</th></tr></thead>
              <tbody>{(data||[]).map((s,i)=><MRow key={i} s={s}/>)}</tbody>
            </table>}
        </div>
      ))}
    </div>
  );
}

function AIBriefing({ indices, sectors, movers, news }) {
  const [text, setText] = useState('');
  const [loading, setLoading] = useState(false);
  const [done, setDone] = useState(false);
  async function generate() {
    if (loading||done) return;
    setLoading(true);
    try {
      const spx=indices?.find(m=>m.code==='SPX'), ndq=indices?.find(m=>m.code==='NDQ'), vix=indices?.find(m=>m.code==='VIX');
      const mkt=`S&P500 ${spx?.price?.toFixed(0)||'N/A'}(${spx?.pct>=0?'+':''}${spx?.pct?.toFixed(2)||'0'}%), NASDAQ ${ndq?.price?.toFixed(0)||'N/A'}(${ndq?.pct>=0?'+':''}${ndq?.pct?.toFixed(2)||'0'}%), VIX ${vix?.price?.toFixed(2)||'N/A'}`;
      const topSec=(sectors||[]).sort((a,b)=>b.chg-a.chg).slice(0,3).map(s=>s.name+'('+(s.chg>=0?'+':'')+s.chg.toFixed(2)+'%)').join('、');
      const botSec=(sectors||[]).sort((a,b)=>a.chg-b.chg).slice(0,2).map(s=>s.name+'('+s.chg.toFixed(2)+'%)').join('、');
      const hl=(news||[]).slice(0,4).map(n=>n.title).join('；');
      const msgs=[{role:'user',content:`你是 Formosa & Co. 的華爾街資深編輯，今日市場數據如下——\n\n大盤：${mkt}\n領漲板塊：${topSec}\n落後板塊：${botSec}\n今日重要新聞：${hl||'暫無'}\n\n請用繁體中文，300字以內，以專業、簡練的財經編輯語氣，條列出三個今日華爾街最值得關注的市場觀察點。格式：每點用「·」開頭，不要開場白。`}];
      const reply=await window.claudeComplete({messages:msgs});
      setText(reply||''); setDone(true);
    } catch(e) { setText('AI 簡報生成失敗，請稍後再試。'); setDone(true); }
    setLoading(false);
  }
  return (
    <div className="briefing-box">
      <div className="briefing-head">
        <div>
          <div className="briefing-title">Wall Street Morning Briefing · 晨間簡報</div>
          <div style={{fontSize:'var(--fs-nano)',color:'var(--ink-mute)',fontStyle:'italic',marginTop:2}}>Powered by Claude AI · Formosa &amp; Co.</div>
        </div>
        <button className="briefing-btn" onClick={generate} disabled={loading||done}>
          {loading?<><span className="agent-dots"><span className="agent-dot"/><span className="agent-dot"/><span className="agent-dot"/></span>&nbsp;生成中</>:done?'已完成':'生成簡報'}
        </button>
      </div>
      <div className="briefing-body">
        {!text&&!loading&&<div className="briefing-placeholder">點擊「生成簡報」讓 AI 分析今日華爾街市場動態</div>}
        {loading&&!text&&<div className="briefing-placeholder"><span className="agent-dots"><span className="agent-dot"/><span className="agent-dot"/><span className="agent-dot"/></span>&nbsp;AI 正在分析市場數據…</div>}
        {text&&<div className="briefing-text" style={{whiteSpace:'pre-line'}}>{text}</div>}
      </div>
    </div>
  );
}

function USNews({ items }) {
  if (!items) return <div className="loading-box">Loading news…</div>;
  if (!items.length) return <div className="loading-box" style={{fontStyle:'italic',color:'var(--ink-mute)'}}>No recent news available.</div>;
  return (
    <div className="us-news-list">
      {items.map((item, i) => (
        <a key={i} className="us-news-item" href={item.link} target="_blank" rel="noopener">
          <div className="us-news-num">{String(i+1).padStart(2,'0')}</div>
          <div>
            <div className="us-news-title">{item.title}</div>
            <div className="us-news-meta">{item.source&&<span>{item.source} · </span>}{item.pubDate?new Date(item.pubDate).toLocaleDateString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}):''}</div>
          </div>
          {item.sentiment&&item.sentiment!=='neutral'&&<div className={`us-news-badge ${item.sentiment}`}>{item.sentiment}</div>}
        </a>
      ))}
    </div>
  );
}

/* ═══════════════════════════════════════════════════════
   總統週期羅盤 — Midterm Year Roadmap
   純日期規則計算，無 API、無 AI。
   統計來源：Stock Trader's Almanac（S&P 500 1950–2024、
   道瓊 1924 以來型態一致）。樣式 pc-* 在 styles.css。
   ═══════════════════════════════════════════════════════ */
function ElectionCycleRoadmap(){
  const now = new Date();

  /* —— 2026 S&P 500 實際 YTD 路徑（疊在歷史合成路徑上，actual vs composite）——
     us_quote 回 Yahoo 原始 JSON；基準=2025 年最後一個收盤 */
  const [actual, setActual] = React.useState(null);
  React.useEffect(() => {
    if (now.getFullYear() !== 2026) return;
    let dead = false;
    fetch('/api/stock?type=us_quote&code=%5EGSPC&range=1y')
      .then(r => r.json())
      .then(raw => {
        const res = raw?.chart?.result?.[0];
        const ts = res?.timestamp || [];
        const cl = res?.indicators?.quote?.[0]?.close || [];
        const rows = ts.map((t, i) => ({ d: new Date(t * 1000), c: cl[i] })).filter(r => r.c > 0);
        const prev = rows.filter(r => r.d.getFullYear() < 2026);
        const cur  = rows.filter(r => r.d.getFullYear() === 2026);
        const base = prev.length ? prev[prev.length - 1].c : (cur[0] && cur[0].c);
        if (!base || !cur.length || dead) return;
        setActual(cur.map(r => ({
          m: r.d.getMonth() + (r.d.getDate() - 1) / 31,
          v: +((r.c / base - 1) * 100).toFixed(2),
        })));
      })
      .catch(() => {});
    return () => { dead = true; };
  }, []);

  /* —— 四年總統週期（S&P 500 1950–2024 平均年報酬）—— */
  const CYCLE_YEARS = [
    { y:2025, nm:'選後年', en:'Post-Election', avg:'+6.6%',  note:'新政磨合・蜜月退潮' },
    { y:2026, nm:'期中年', en:'Midterm',       avg:'+4.6%',  note:'四年最弱・回檔最深' },
    { y:2027, nm:'選前年', en:'Pre-Election',  avg:'+16.8%', note:'四年最強・拉抬行情' },
    { y:2028, nm:'大選年', en:'Election',      avg:'+7.3%',  note:'多空拉鋸・選情定調' },
  ];
  const cycleStart = new Date(2025,0,1).getTime();
  const cycleEnd   = new Date(2029,0,1).getTime();
  const cyclePos = Math.min(1, Math.max(0, (now.getTime()-cycleStart)/(cycleEnd-cycleStart)));

  /* —— 期中年 12 個月平均累積報酬路徑（%，月底值；月中線性內插）——
     四種子樣本（Stock Trader's Almanac 2026 期中年合成圖）：
     2026 同時屬於前三類（川普第2任期中年＝在任第6年＝共和黨第2任期中） */
  const SERIES = [
    { nm:'所有期中年',       en:'All Midterm Years',       c:'#1a2438', w:2.4, dash:'',
      cum:[0, 0.9, 0.3, 0.9, 0.5, -0.6, -1.7, -1.2, -2.1, -3.1, -0.7, 1.9, 4.0] },
    { nm:'總統第6年',        en:'6th Year of Presidency',  c:'#a63d3d', w:1.5, dash:'',
      cum:[0, 0.7, -0.2, 0.5, 0.0, -1.2, -2.3, -1.5, -2.6, -3.4, -0.4, 1.4, 3.2] },
    { nm:'共和黨第2任期中',  en:'2nd Republican Midterms', c:'#2f6b4f', w:1.5, dash:'',
      cum:[0, -0.8, 1.5, 3.5, 6.0, 7.0, 6.0, 4.5, 2.5, 1.0, 3.5, 6.0, 8.5] },
    { nm:'STA 綜合週期',     en:'STA Aggregate Cycle',     c:'#b8924d', w:1.8, dash:'7 4',
      cum:[0, 1.0, 0.6, 1.2, 0.8, 0.2, -0.5, -0.2, -0.8, -1.2, 0.6, 2.4, 4.4] },
  ];
  const CUM = SERIES[0].cum;
  const monthPos = now.getFullYear()===2026
    ? now.getMonth() + (now.getDate()-1)/31
    : null;

  /* —— 期中年內的歷史階段 —— */
  const PHASES = [
    { a:0,   b:3,   nm:'開局震盪',       c:'rgba(26,36,56,.045)' },
    { a:3,   b:8.5, nm:'週期低谷風險區', c:'rgba(166,61,61,.08)' },
    { a:8.5, b:10,  nm:'傳統落底窗',     c:'rgba(184,146,77,.16)' },
    { a:10,  b:12,  nm:'Q4 起漲',        c:'rgba(47,107,79,.09)' },
  ];
  const phaseOf = m => PHASES.find(p => m>=p.a && m<p.b) || PHASES[PHASES.length-1];

  /* —— SVG 幾何（2026-07-20 升級：加高、右緣線尾標籤、y 域含實際走勢動態擴展）—— */
  const W=1000, H=330, padL=48, padR=128, padT=20, padB=36;
  const actMin = actual ? Math.min(...actual.map(a=>a.v)) : 0;
  const actMax = actual ? Math.max(...actual.map(a=>a.v)) : 0;
  const ymin = Math.min(-4.6, Math.floor(actMin) - 1);
  const ymax = Math.max(9.6, Math.ceil(actMax) + 1);
  const X = m => padL + (W-padL-padR)*m/12;
  const Y = v => padT + (H-padT-padB)*(ymax-v)/(ymax-ymin);
  /* Catmull-Rom 平滑曲線（月度合成路徑用；直線折角是「陽春感」主因之一） */
  const smoothPath = cum => {
    const pts = cum.map((v,i)=>[X(i), Y(v)]);
    if (pts.length < 3) return pts.map((p,i)=>`${i?'L':'M'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' ');
    let d = `M${pts[0][0].toFixed(1)},${pts[0][1].toFixed(1)}`;
    for (let i = 0; i < pts.length - 1; i++) {
      const p0 = pts[Math.max(0, i-1)], p1 = pts[i], p2 = pts[i+1], p3 = pts[Math.min(pts.length-1, i+2)];
      const c1 = [p1[0] + (p2[0]-p0[0])/6, p1[1] + (p2[1]-p0[1])/6];
      const c2 = [p2[0] - (p3[0]-p1[0])/6, p2[1] - (p3[1]-p1[1])/6];
      d += ` C${c1[0].toFixed(1)},${c1[1].toFixed(1)} ${c2[0].toFixed(1)},${c2[1].toFixed(1)} ${p2[0].toFixed(1)},${p2[1].toFixed(1)}`;
    }
    return d;
  };
  const pathOf = smoothPath;
  const actualPath = actual
    ? actual.map((a,i)=>`${i?'L':'M'}${X(a.m).toFixed(1)},${Y(a.v).toFixed(1)}`).join(' ')
    : null;
  const actualLast = actual ? actual[actual.length-1] : null;

  let nowX=null, nowY=null;
  if (monthPos!=null){
    const i = Math.min(11, Math.floor(monthPos)), f = monthPos-i;
    const nowV = CUM[i] + (CUM[i+1]-CUM[i])*f;
    nowX = X(monthPos); nowY = Y(nowV);
  }
  const electionX = X(10 + 2/30);  /* 2026-11-03 期中選舉 */
  const daysToElection = Math.max(0, Math.ceil((new Date(2026,10,3).getTime()-now.getTime())/86400000));

  const curPhase = monthPos!=null ? phaseOf(monthPos) : null;
  const verdict = monthPos!=null
    ? `目前位置：期中選舉年第 ${now.getMonth()+1} 月（${curPhase.nm}）。歷史平均路徑上，4 月起走弱、9–10 月落底、Q4 反轉起漲；距離 11/3 期中選舉尚有 ${daysToElection} 天，距傳統落底窗（9 月中–10 月）約 ${Math.max(0, Math.round(8.5-monthPos))} 個月。注意：2026 同時是「總統第6年」（紅線）與「共和黨第2任期中年」（綠線）——後者子樣本上半年明顯偏強、9 月才壓回 +1% 低點，年底衝高逾 +8%。低點後通常開啟整個四年週期最強的「甜蜜三季」（期中年Q4＋選前年Q1、Q2）。`
    : `${now.getFullYear()} 年不在期中選舉年；下一個期中年為 2030。`;

  return (
    <div className="pc-wrap">
      <div className="pc-inner">
        <div className="pc-lede">
          1924–2024 百年間共 25 個期中選舉年。期中年是四年總統週期中報酬最弱、回檔最深的一年，
          但歷史型態高度一致：上半年震盪走弱 → 9–10 月落底 → 第四季強勁反轉，
          並開啟整個週期最可靠的多頭段。
        </div>

        {/* 四年週期定位 */}
        <div className="pc-cycle">
          {CYCLE_YEARS.map(c => (
            <div key={c.y} className={`pc-seg ${c.y===now.getFullYear()?'now':''}`}>
              <span className="pc-seg-y num">{c.y}</span>
              <div className="pc-seg-nm">{c.nm} <span className="pc-seg-en">{c.en}</span></div>
              <div className="pc-seg-avg num" style={{color:parseFloat(c.avg)>=10?'var(--bull,#2f6b4f)':'var(--ink)'}}>{c.avg}</div>
              <div className="pc-seg-note">{c.note}</div>
            </div>
          ))}
        </div>
        <div className="pc-cycle-track">
          <div className="pc-cycle-pin" style={{left:`${(cyclePos*100).toFixed(2)}%`}}>▲ 現在</div>
        </div>

        {/* 期中年季節路徑 */}
        <svg viewBox={`0 0 ${W} ${H}`} style={{width:'100%',display:'block'}}>
          {PHASES.map((p,i)=>(
            <g key={i}>
              <rect x={X(p.a)} y={padT} width={X(p.b)-X(p.a)} height={H-padT-padB} fill={p.c}/>
              <text x={(X(p.a)+X(p.b))/2} y={padT+14} textAnchor="middle"
                    fontSize="11.5" fill="var(--ink-mute)" fontFamily="'BiauKai','標楷體',serif" letterSpacing="2">{p.nm}</text>
            </g>
          ))}
          {Array.from({length:13},(_,m)=>(
            <g key={m}>
              <line x1={X(m)} y1={padT} x2={X(m)} y2={H-padB} stroke="var(--paper-line)" strokeWidth="1" opacity=".55"/>
              {m<12 && <text x={X(m+0.5)} y={H-padB+18} textAnchor="middle" fontSize="12"
                    fill="var(--ink-soft)" fontFamily="'Times New Roman',serif">{m+1}月</text>}
            </g>
          ))}
          {(() => {
            const step = (ymax - ymin) > 22 ? 4 : 2;
            const ticks = [];
            for (let v = Math.ceil(ymin/step)*step; v <= ymax; v += step) ticks.push(v);
            return ticks.map(v=>(
              <g key={v}>
                <line x1={padL} y1={Y(v)} x2={W-padR} y2={Y(v)} stroke={v===0?'var(--ink-mute)':'var(--paper-line)'}
                      strokeWidth="1" strokeDasharray={v===0?'':'2 4'}/>
                <text x={padL-7} y={Y(v)+4} textAnchor="end" fontSize="11" fill="var(--ink-mute)"
                      fontFamily="'Times New Roman',serif">{v>0?'+':''}{v}%</text>
              </g>
            ));
          })()}
          <line x1={electionX} y1={padT} x2={electionX} y2={H-padB} stroke="var(--ink)" strokeWidth="1" strokeDasharray="5 4"/>
          <text x={electionX+5} y={H-padB-10} fontSize="11" fill="var(--ink)"
                fontFamily="'BiauKai','標楷體',serif" letterSpacing="1">期中選舉 11/3</text>
          {/* 歷史合成路徑（Catmull-Rom 平滑）＋右緣線尾標籤 */}
          {SERIES.map((s,i)=>(
            <g key={i}>
              <path d={pathOf(s.cum)} fill="none" stroke={s.c} strokeWidth={s.w}
                    strokeDasharray={s.dash} strokeLinejoin="round" strokeLinecap="round"
                    opacity={i===0?1:.8}/>
              <text x={W-padR+8} y={Y(s.cum[12]) + (i===3?12:4)} fontSize="11" fill={s.c}
                    fontFamily="'BiauKai','標楷體',serif" opacity=".9">{s.nm} {s.cum[12]>0?'+':''}{s.cum[12]}%</text>
            </g>
          ))}
          <circle cx={X(9)} cy={Y(CUM[9])} r="3.5" fill="none" stroke="var(--bear,#a63d3d)" strokeWidth="1.6"/>
          <text x={X(9)} y={Y(CUM[9])+20} textAnchor="middle" fontSize="11" fill="var(--bear,#a63d3d)"
                fontFamily="'BiauKai','標楷體',serif">歷史平均低點</text>
          {/* 2026 實際走勢（actual vs composite——本圖的研究核心） */}
          {actualPath && (
            <g>
              <path d={actualPath} fill="none" stroke="var(--gold-deep,#8a6a2a)" strokeWidth="2.8"
                    strokeLinejoin="round" strokeLinecap="round"/>
              {actualLast && (
                <g>
                  <circle cx={X(actualLast.m)} cy={Y(actualLast.v)} r="4.5" fill="var(--gold-deep,#8a6a2a)"
                          stroke="var(--paper-bright,#fff)" strokeWidth="1.5"/>
                  <text x={Math.min(X(actualLast.m)+8, W-padR+8)} y={Y(actualLast.v)-8} fontSize="12.5"
                        fill="var(--gold-deep,#8a6a2a)" fontFamily="'BiauKai','標楷體',serif" fontWeight="bold">
                    2026 實際 {actualLast.v>0?'+':''}{actualLast.v}%
                  </text>
                </g>
              )}
            </g>
          )}
          {nowX!=null && (
            <g>
              <line x1={nowX} y1={padT} x2={nowX} y2={H-padB} stroke="var(--gold)" strokeWidth="1.4" opacity=".8"/>
              <circle cx={nowX} cy={nowY} r="4.5" fill="var(--gold)" stroke="var(--paper-bright,#fff)" strokeWidth="1.5"/>
              <text x={nowX} y={padT-6} textAnchor="middle" fontSize="12.5" fill="var(--gold-deep,#8a6a32)"
                    fontFamily="'BiauKai','標楷體',serif" fontWeight="bold" letterSpacing="1">
                ▼ 現在 {now.getMonth()+1}/{now.getDate()}
              </text>
            </g>
          )}
        </svg>

        <div className="pc-legend">
          {actual && (
            <div className="pc-leg-item">
              <span className="pc-leg-swatch" style={{background:'var(--gold-deep,#8a6a2a)', height:4}}/>
              <span className="pc-leg-nm" style={{fontWeight:700}}>2026 實際走勢</span>
              <span className="pc-leg-en">S&P 500 YTD (Actual)</span>
            </div>
          )}
          {SERIES.map((s,i)=>(
            <div key={i} className="pc-leg-item">
              <span className="pc-leg-swatch" style={{background:s.dash?'none':s.c, borderTop:s.dash?`2.5px dashed ${s.c}`:'none', height:s.dash?0:3}}/>
              <span className="pc-leg-nm">{s.nm}</span>
              <span className="pc-leg-en">{s.en}</span>
            </div>
          ))}
        </div>

        <div className="pc-verdict">◈ {verdict}</div>

        <div className="pc-stats">
          <div className="pc-stat"><div className="pc-stat-v num">+4.6%</div><div className="pc-stat-l">期中年平均報酬<br/>四年週期最弱</div></div>
          <div className="pc-stat"><div className="pc-stat-v num" style={{color:'var(--bear,#a63d3d)'}}>−17.3%</div><div className="pc-stat-l">平均高低點回檔<br/>歷時約 115 天</div></div>
          <div className="pc-stat"><div className="pc-stat-v num" style={{color:'var(--bull,#2f6b4f)'}}>+31.7%</div><div className="pc-stat-l">期中年低點後<br/>12 個月平均反彈</div></div>
          <div className="pc-stat"><div className="pc-stat-v num">100%</div><div className="pc-stat-l">1950 以來期中選舉後<br/>12 個月皆收高</div></div>
          <div className="pc-stat"><div className="pc-stat-v num">{daysToElection}天</div><div className="pc-stat-l">距 2026/11/3<br/>期中選舉</div></div>
        </div>

        <div className="pc-note">
          * 統計：Stock Trader's Almanac — S&P 500 四種子樣本合成路徑（1949–2024）：所有期中年／總統第6年／共和黨第2任期中年／STA綜合週期；
          期中年平均回檔 −17.3%（約115天），低點後12個月平均反彈 +31.7%；道瓊 1924 年以來型態一致。歷史規律僅供參考，不構成投資建議。
        </div>
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════
// 美股五力排行榜（移植自台股 stock-lab.jsx calcLiteScore）
// ════════════════════════════════════════════════════════
function avg5(arr) { return arr.length ? arr.reduce((s,v)=>s+v,0)/arr.length : 0; }

function computeEMA5(arr, p) {
  const k = 2/(p+1); const res = []; let ema = null;
  for (let i=0; i<arr.length; i++) {
    if (i < p-1) { res.push(null); continue; }
    if (ema === null) ema = avg5(arr.slice(0,p));
    else ema = arr[i]*k + ema*(1-k);
    res.push(ema);
  }
  return res;
}

function computeRSI5(closes, period=14) {
  const res = Array(closes.length).fill(null);
  if (closes.length < period+1) return res;
  let gains=0, losses=0;
  for (let i=1; i<=period; i++) { const d=closes[i]-closes[i-1]; if (d>0) gains+=d; else losses-=d; }
  let ag=gains/period, al=losses/period;
  res[period] = al===0 ? 100 : parseFloat((100-100/(1+ag/al)).toFixed(2));
  for (let i=period+1; i<closes.length; i++) {
    const d = closes[i]-closes[i-1];
    ag = (ag*(period-1)+Math.max(d,0))/period;
    al = (al*(period-1)+Math.max(-d,0))/period;
    res[i] = al===0 ? 100 : parseFloat((100-100/(1+ag/al)).toFixed(2));
  }
  return res;
}

function prepCandlesUS(raw) {
  const res = raw?.chart?.result?.[0]; if (!res) return [];
  const ts = res.timestamp || [];
  const q = res.indicators?.quote?.[0] || {};
  return ts.map((t,i) => ({
    close: +(q.close?.[i] ?? 0), volume: +(q.volume?.[i] ?? 0), open: +(q.open?.[i] ?? 0),
  })).filter(d => d.close > 0).slice(-300);
}

// calcLiteScore 技術五力（最高 67 分），純技術面、跨市場通用
function calcLiteScoreUS(candles, benchCloses) {
  if (!candles || candles.length < 60) return null;
  const closes = candles.map(c=>c.close);
  const n = closes.length;
  const cl = closes[n-1];
  if (!cl || cl<=0) return null;

  const sma = (period, back) => {
    const b = back||0;
    if (n-b < period) return null;
    let s=0; for (let i=n-b-period; i<n-b; i++) s+=closes[i];
    return s/period;
  };

  const ma50=sma(50), ma150=sma(150), ma200=sma(200), ma200p=sma(200,21);
  const w52 = closes.slice(Math.max(0,n-252));
  const hi52 = Math.max(...w52), lo52 = Math.min(...w52);
  let upVol=0, dnVol=0;
  candles.slice(-50).forEach((d,i,a) => {
    const p = i>0 ? a[i-1].close : d.open;
    if (d.close >= p) upVol += d.volume; else dnVol += d.volume;
  });
  const ttChecks = [
    { pts:3, ok: ma50!=null ? cl>ma50 : null },
    { pts:3, ok: ma150!=null ? cl>ma150 : null },
    { pts:3, ok: ma200!=null ? cl>ma200 : null },
    { pts:4, ok: (ma50&&ma150&&ma200) ? ma50>ma150 && ma150>ma200 : null },
    { pts:3, ok: (ma200&&ma200p) ? ma200>ma200p : null },
    { pts:3, ok: lo52>0 ? cl >= lo52*1.30 : null },
    { pts:3, ok: hi52>0 ? cl >= hi52*0.75 : null },
    { pts:3, ok: upVol > dnVol },
  ];
  const pTrend = ttChecks.reduce((s,t) => s + (t.ok===true ? t.pts : t.ok===null ? Math.round(t.pts/3) : 0), 0);

  const wRet = (arr) => {
    const m = arr.length;
    const pct = back => m>=back && arr[m-back]>0 ? (arr[m-1]-arr[m-back])/arr[m-back]*100 : null;
    const r3 = pct(64); if (r3 == null) return null;
    let total = 0.4*r3, w = 0.4;
    const r6=pct(128), r9=pct(192), r12=pct(252);
    if (r6!=null) { total+=0.2*r6; w+=0.2; }
    if (r9!=null) { total+=0.2*r9; w+=0.2; }
    if (r12!=null) { total+=0.2*r12; w+=0.2; }
    return total/w;
  };
  const stockRet = wRet(closes);
  const benchRet = benchCloses && benchCloses.length>=64 ? wRet(benchCloses) : null;
  const alpha = stockRet != null ? (benchRet != null ? stockRet-benchRet : stockRet-8) : null;
  const rsPts = alpha == null ? 5 : alpha>=30?12:alpha>=15?10:alpha>=5?8:alpha>=0?6:alpha>=-10?3:0;

  const ema12 = computeEMA5(closes,12), ema26 = computeEMA5(closes,26);
  const difA = closes.map((_,i) => ema12[i]!=null && ema26[i]!=null ? ema12[i]-ema26[i] : null);
  const validDif = difA.filter(v=>v!=null);
  let macdPts = 2;
  if (validDif.length >= 10) {
    const dea9 = computeEMA5(validDif,9);
    const deaFull = Array(n).fill(null); let vi=0;
    for (let i=0;i<n;i++) { if (difA[i]!=null) deaFull[i] = dea9[vi++]; }
    const difNow=difA[n-1], deaNow=deaFull[n-1], difPv=difA[n-2], deaPv=deaFull[n-2];
    if (difNow!=null && deaNow!=null && difPv!=null && deaPv!=null) {
      const hNow=difNow-deaNow, hPv=difPv-deaPv;
      const gx = difPv<deaPv && difNow>=deaNow;
      const dx = difPv>deaPv && difNow<=deaNow;
      macdPts = dx?0 : gx?4 : (difNow>deaNow && deaNow>0 && hNow>hPv)?4 : (difNow>deaNow && deaNow>0)?3 : difNow>deaNow?2:1;
    }
  }

  const rsiSeries = computeRSI5(closes);
  const rsiNow = rsiSeries.filter(v=>v!=null).slice(-1)[0] || 50;
  const rsiPts = rsiNow>=55 && rsiNow<70 ? 4 : rsiNow>=45 && rsiNow<55 ? 3 : (rsiNow>=70 || (rsiNow>=30 && rsiNow<45)) ? 2 : 1;

  const pMom = rsPts + macdPts + rsiPts;
  const pChip = 6, pQual = 8, pVal = 8; // 美股無三大法人/財報 batch，中性基準
  const raw = pTrend + pMom + pChip + pQual + pVal;
  const total = ma200 != null && cl < ma200 ? Math.min(raw, 54) : raw;

  return { total, pTrend, pMom, alpha: alpha!=null ? +alpha.toFixed(1) : null, rsiNow: +rsiNow.toFixed(1) };
}

const FIVEPOWER_UNIVERSE_US = [
  'AAPL','MSFT','NVDA','GOOGL','AMZN','META','TSLA','AVGO','AMD','ORCL','CRM','ADBE','NOW','SNOW','PLTR',
  'NFLX','DIS','PARA','CMCSA','SPOT',
  'JPM','GS','MS','BAC','WFC','V','MA','AXP','PYPL','SQ','COIN',
  'UNH','LLY','REGN','AMGN','ABBV','MRK','ISRG','JNJ','PFE',
  'XOM','CVX','COP','SLB',
  'WMT','COST','TGT','HD','MCD','KO','PEP','NKE',
  'INTC','QCOM','TXN','MU','AMAT','KLAC','LRCX','ASML',
  'CRWD','PANW','ZS','NET','DDOG',
  'RIVN','F','GM','UBER','ABNB','SHOP',
];

function FivePowerScanUS() {
  const [universe, setUniverse] = useState(() => FIVEPOWER_UNIVERSE_US.map(code => ({ code, loaded:false })));
  const [doneCount, setDoneCount] = useState(0);
  const [state, setState] = useState('idle'); // idle | scanning | done | error
  const [filter, setFilter] = useState('all');
  const benchRef = useRef(null);
  const abortRef = useRef(false);

  const startScan = useCallback(async () => {
    abortRef.current = false;
    setState('scanning');
    setDoneCount(0);
    setUniverse(FIVEPOWER_UNIVERSE_US.map(code => ({ code, loaded:false })));

    // 基準：S&P 500（^GSPC）
    try {
      const r = await fetch('/api/stock?type=us_quote&code=%5EGSPC&range=2y&interval=1d');
      const j = await r.json();
      benchRef.current = prepCandlesUS(j).map(d=>d.close);
    } catch(e) { benchRef.current = []; }

    const BATCH = 5, DELAY = 200;
    const codes = FIVEPOWER_UNIVERSE_US;
    let done = 0;
    for (let i=0; i<codes.length; i += BATCH) {
      if (abortRef.current) break;
      const batch = codes.slice(i, i+BATCH);
      const results = await Promise.allSettled(batch.map(code =>
        fetch(`/api/stock?type=us_quote&code=${code}&range=2y&interval=1d`)
          .then(r=>r.ok?r.json():null)
          .then(j => ({ code, candles: prepCandlesUS(j) }))
          .catch(() => ({ code, candles: [] }))
      ));
      const updates = {};
      results.forEach(res => {
        if (res.status !== 'fulfilled') return;
        const { code, candles } = res.value;
        if (candles.length >= 60) {
          const last = candles[candles.length-1], prev = candles[candles.length-2];
          updates[code] = {
            candles, loaded:true,
            price: last?.close || 0,
            chg: prev?.close ? (last.close-prev.close)/prev.close*100 : 0,
          };
        }
      });
      done += batch.length;
      setDoneCount(done);
      setUniverse(prev => prev.map(s => updates[s.code] ? { ...s, ...updates[s.code] } : s));
      if (i+BATCH < codes.length) await new Promise(r=>setTimeout(r, DELAY));
    }
    setState('done');
  }, []);

  const ranked = useMemo(() => {
    if (state !== 'done') return [];
    return universe
      .filter(s => s.loaded && s.candles.length >= 60)
      .map(s => { const sc = calcLiteScoreUS(s.candles, benchRef.current); return sc ? { ...s, sc } : null; })
      .filter(Boolean)
      .sort((a,b) => b.sc.total - a.sc.total);
  }, [universe, state]);

  const displayed = useMemo(() => {
    if (filter === 'bull')  return ranked.filter(s => s.sc.total >= 55);
    if (filter === 'trend') return ranked.filter(s => s.sc.pTrend >= 20);
    return ranked;
  }, [ranked, filter]);

  const vdLabel = total =>
    total>=60 ? { l:'強力買進', c:'var(--bull)' } :
    total>=52 ? { l:'偏多',     c:'#3a8a5a' } :
    total>=42 ? { l:'中性觀察', c:'var(--gold)' } :
    { l:'偏空', c:'var(--bear)' };

  if (state === 'idle') return (
    <div style={{ textAlign:'center', padding:'30px 0' }}>
      <div style={{ fontSize:13, color:'var(--ink-mute)', fontStyle:'italic', marginBottom:14, fontFamily:"'Times New Roman',serif" }}>
        對 {FIVEPOWER_UNIVERSE_US.length} 檔主力美股進行技術五力批次評分（趨勢樣板＋相對動能 RS／MACD／RSI），約需 20–40 秒
      </div>
      <button onClick={startScan} style={{
        padding:'8px 24px', border:'1px solid var(--ink)', background:'var(--ink)', color:'var(--paper-bright)',
        cursor:'pointer', fontFamily:"'Times New Roman',serif", fontVariant:'small-caps', letterSpacing:'.14em', fontSize:13,
      }}>開始掃描 Scan</button>
    </div>
  );

  if (state === 'scanning') return (
    <div style={{ textAlign:'center', padding:'30px 0', color:'var(--ink-mute)', fontStyle:'italic', fontFamily:"'Times New Roman',serif" }}>
      掃描中… {doneCount} / {FIVEPOWER_UNIVERSE_US.length}
    </div>
  );

  return (
    <div>
      <div style={{ display:'flex', gap:8, marginBottom:16, flexWrap:'wrap', alignItems:'center' }}>
        {[
          { id:'all',   label:'全部排行',     cnt: ranked.length },
          { id:'bull',  label:'強多以上 ≥55', cnt: ranked.filter(s=>s.sc.total>=55).length },
          { id:'trend', label:'趨勢完美 ≥20', cnt: ranked.filter(s=>s.sc.pTrend>=20).length },
        ].map(f => (
          <button key={f.id} onClick={()=>setFilter(f.id)} style={{
            padding:'5px 14px', border:'1px solid var(--paper-line)', cursor:'pointer',
            background: filter===f.id ? 'var(--gold)' : 'var(--paper-bright)',
            color: filter===f.id ? '#fff' : 'var(--ink-mute)',
            fontFamily:"'Times New Roman',serif", fontSize:'var(--fs-nano)', letterSpacing:'.06em',
          }}>{f.label} ({f.cnt})</button>
        ))}
        <button onClick={startScan} style={{ marginLeft:'auto', fontSize:'var(--fs-nano)', color:'var(--gold-deep)', background:'none', border:'none', cursor:'pointer', textDecoration:'underline', fontFamily:"'Times New Roman',serif" }}>↻ 重新掃描</button>
      </div>
      <div style={{ overflowX:'auto' }}>
        <table style={{ width:'100%', borderCollapse:'collapse', fontSize:12, fontFamily:"'Times New Roman',serif" }}>
          <thead>
            <tr style={{ background:'var(--paper-deep)' }}>
              {['#','代號','收盤','漲跌%','評估','五力分 /67','趨勢/25','α%','RSI'].map((h,i) => (
                <th key={h} style={{ padding:'6px 8px', textAlign: i<=1?'left':'right', fontSize:'var(--fs-nano)', color:'var(--ink-mute)', letterSpacing:'.1em', borderBottom:'2px solid var(--ink)' }}>{h}</th>
              ))}
            </tr>
          </thead>
          <tbody>
            {displayed.map((s,idx) => {
              const vd = vdLabel(s.sc.total);
              const barW = Math.round(s.sc.total/67*100);
              return (
                <tr key={s.code} style={{ borderBottom:'1px solid var(--paper-line)' }}>
                  <td style={{ padding:'6px 8px', color:'var(--ink-mute)' }}>{idx+1}</td>
                  <td style={{ padding:'6px 8px' }}>
                    <a href={`美股個股.html?code=${s.code}`} style={{ color:'var(--gold-deep)', fontWeight:700, textDecoration:'none' }}>{s.code}</a>
                    {zh(s.code) && <span style={{ marginLeft:6, fontFamily:"'BiauKai','標楷體',serif", fontSize:'var(--fs-nano)', color:'var(--ink-mute)' }}>{zh(s.code)}</span>}
                  </td>
                  <td style={{ padding:'6px 8px', textAlign:'right', fontWeight:700 }}>${fmtN(s.price)}</td>
                  <td style={{ padding:'6px 8px', textAlign:'right', color: s.chg>=0?'var(--bull)':'var(--bear)' }}>{s.chg>=0?'+':''}{fmtN(s.chg)}%</td>
                  <td style={{ padding:'6px 8px', textAlign:'right' }}><span style={{ fontSize:'var(--fs-nano)', padding:'2px 7px', color:vd.c, border:`1px solid ${vd.c}`, borderRadius:3 }}>{vd.l}</span></td>
                  <td style={{ padding:'6px 8px' }}>
                    <div style={{ display:'flex', alignItems:'center', gap:6 }}>
                      <div style={{ flex:1, minWidth:60, height:6, background:'var(--paper-line)', borderRadius:3, overflow:'hidden' }}>
                        <div style={{ width:barW+'%', height:'100%', background: s.sc.total>=55?'var(--bull)':s.sc.total>=42?'var(--gold)':'var(--bear)' }}/>
                      </div>
                      <span style={{ fontWeight:700 }}>{s.sc.total}</span>
                    </div>
                  </td>
                  <td style={{ padding:'6px 8px', textAlign:'right', fontWeight: s.sc.pTrend>=20?700:400, color: s.sc.pTrend>=20?'var(--bull)':'inherit' }}>{s.sc.pTrend}</td>
                  <td style={{ padding:'6px 8px', textAlign:'right', color: s.sc.alpha>0?'var(--bull)':s.sc.alpha<0?'var(--bear)':'var(--ink-mute)' }}>{s.sc.alpha!=null?(s.sc.alpha>0?'+':'')+s.sc.alpha+'%':'—'}</td>
                  <td style={{ padding:'6px 8px', textAlign:'right' }}>{Math.round(s.sc.rsiNow)}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
      <div style={{ marginTop:12, fontSize:'var(--fs-nano)', color:'var(--ink-mute)', lineHeight:1.8, background:'var(--paper-deep)', padding:'8px 14px' }}>
        ※ 技術五力評分（最高 67 分）：趨勢樣板 Minervini 8 準則 25 ＋ 相對動能 RS/MACD/RSI 20 ＋ 法人／財務／估值三柱中性基準 22。
        基準為 S&amp;P 500（^GSPC）；跌破 MA200 年線者上限 54 分。
      </div>
    </div>
  );
}

function USDashboard() {
  const [indices, setIndices] = useState(null);
  const [sectors, setSectors] = useState(null);
  const [movers,  setMovers]  = useState(null);
  const [news,    setNews]    = useState(null);
  const [macro,   setMacro]   = useState(null);

  // QuoteHub v2 訂閱:原本只在載入時抓一次,現在美股盤中自動更新、收盤降頻、週末停輪
  useEffect(() => {
    const unsubs = [
      window.QuoteHub.subscribe({ type: 'us_indices', onData: d => setIndices(d.indices || []) }),
      window.QuoteHub.subscribe({ type: 'us_sectors', onData: d => setSectors(d.sectors || []) }),
      window.QuoteHub.subscribe({ type: 'us_movers',  onData: d => setMovers(d) }),
      window.QuoteHub.subscribe({ type: 'us_news',    onData: d => setNews(d.items || []) }),
      window.QuoteHub.subscribe({ type: 'oddities',   onData: d => setMacro(d) })
    ];
    return () => unsubs.forEach(u => u());
  }, []);

  const spx=indices?.find(m=>m.code==='SPX'), ndq=indices?.find(m=>m.code==='NDQ');
  const dji=indices?.find(m=>m.code==='DJI'), sox=indices?.find(m=>m.code==='SOX');
  const vix=indices?.find(m=>m.code==='VIX'), tnx=indices?.find(m=>m.code==='TNX');
  const statItems=[vix,tnx,sox].filter(Boolean);

  return (
    <>
      <USTicker indices={indices||[]} macro={macro}/>

      <div className="us-cover">
        <div>
          <div className="us-cover-title">Wall <span className="amp">&amp;</span> Street<span className="sub">美 國 股 市 圖 鑑</span></div>
          <div style={{marginTop:16}}>
            {spx?<IdxChart hist={spx.hist} price={spx.price} chg={spx.chg} pct={spx.pct} label="S&P 500 · 標普500 — 90D"/>:<div className="loading-box" style={{minHeight:160}}>載入中…</div>}
          </div>
          <div className="us-lede" style={{marginTop:14}}>橫跨紐約 · 芝加哥 · 矽谷<br/>三大市場脈動，盡在一紙之間</div>
        </div>
        <div>
          <div className="us-stat-box" style={{marginBottom:14}}>
            <div className="us-stat-head">Key Indicators</div>
            {statItems.length?statItems.map((s,i)=>(
              <div key={i} className="us-stat-row">
                <div><div className="us-stat-nm">{s.name}</div><div className="us-stat-en">{s.en}</div></div>
                <div><div className="us-stat-val num">{fmtN(s.price)}</div><div className={`us-stat-chg ${s.chg>=0?'up':'dn'}`}>{s.chg>=0?'+':''}{fmtN(s.chg)} ({s.pct>=0?'+':''}{fmtN(s.pct)}%)</div></div>
              </div>
            )):[1,2,3].map(i=>(<div key={i} className="us-stat-row"><div className="skel" style={{height:12,width:80,marginBottom:4}}/><div className="skel" style={{height:17,width:60}}/></div>))}
            <div className="us-data-note">* Yahoo Finance · 15-min delayed</div>
          </div>
          <FearGreed vixPrice={vix?.price}/>
        </div>
      </div>

      <div className="idx-trio">
        {ndq&&<IdxChart hist={ndq.hist} price={ndq.price} chg={ndq.chg} pct={ndq.pct} label="NASDAQ · 那斯達克 — 90D"/>}
        {dji&&<IdxChart hist={dji.hist} price={dji.price} chg={dji.chg} pct={dji.pct} label="Dow Jones · 道瓊工業 — 90D"/>}
        {sox&&<IdxChart hist={sox.hist} price={sox.price} chg={sox.chg} pct={sox.pct} label="PHLX SOX · 費半指數 — 90D"/>}
      </div>

      <SectionHead kicker="Macro Backdrop" title="總體環境" titleEn="Global Macro Indicators"/>
      <div style={{marginTop:14}}><MacroBackdrop macro={macro}/></div>
      <SectionHead kicker="Presidential Cycle · 1924–2024" title="總統週期羅盤" titleEn="Midterm Year Roadmap"/>
      <div style={{marginTop:14}}><ElectionCycleRoadmap/></div>
      <SectionHead kicker="GICS Sectors" title="產業板塊" titleEn="Sector Performance"/>
      <div style={{marginTop:14}}><USSectorHeat sectors={sectors}/></div>
      <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:18,marginBottom:28}}>
        <MarketBreadth sectors={sectors}/><FearGreed vixPrice={vix?.price}/>
      </div>
      <SectionHead kicker="Hot Themes" title="熱門主題" titleEn="Trending Investment Themes"/>
      <div style={{marginTop:14}}><HotThemes/></div>
      <SectionHead kicker="Five-Forces Ranking" title="美股五力排行榜" titleEn="Five Forces Scan"/>
      <div style={{marginTop:14,marginBottom:28}}><FivePowerScanUS/></div>
      <PreIPOWatchlist/>
      <SectionHead kicker="Earnings Calendar" title="財報行事曆" titleEn="Upcoming Earnings"/>
      <div style={{marginTop:14}}><EarningsCalendar/></div>
      <SectionHead kicker="Market Movers" title="漲跌排行" titleEn="Today's Top Movers"/>
      <div style={{marginTop:14}}><USMovers gainers={movers?.gainers} losers={movers?.losers}/></div>
      <SectionHead kicker="AI · Claude" title="晨間簡報" titleEn="Morning Briefing"/>
      <div style={{marginTop:14}}><AIBriefing indices={indices} sectors={sectors} movers={movers} news={news}/></div>
      <SectionHead kicker="Wall Street News" title="最新要聞" titleEn="Market Headlines"/>
      <div style={{marginTop:14}}><USNews items={news}/></div>
      <FooterBar/>
    </>
  );
}

window.USDashboardContent = USDashboard;
