// 美股 11 大板塊（SPDR 行業 ETF）— 熱力圖、輪動、資金流向、ETF 規模（機構被動配置 proxy）
import { getHistory } from './marketdata.js';
import { yahooQuoteSummary, resetYahooAuth, sleep as yahooSleep } from './yahoo-session.js';

const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124 Safari/537.36';

export const SECTOR_ETFS = [
  { etf: 'XLK', nameZh: '科技', nameEn: 'Technology', group: 'growth' },
  { etf: 'XLC', nameZh: '通訊服務', nameEn: 'Communication', group: 'growth' },
  { etf: 'XLY', nameZh: '非必需消費', nameEn: 'Cons. Discretionary', group: 'cyclical' },
  { etf: 'XLP', nameZh: '必需消費', nameEn: 'Cons. Staples', group: 'defensive' },
  { etf: 'XLE', nameZh: '能源', nameEn: 'Energy', group: 'cyclical' },
  { etf: 'XLF', nameZh: '金融', nameEn: 'Financials', group: 'cyclical' },
  { etf: 'XLV', nameZh: '醫療保健', nameEn: 'Health Care', group: 'defensive' },
  { etf: 'XLI', nameZh: '工業', nameEn: 'Industrials', group: 'cyclical' },
  { etf: 'XLB', nameZh: '原物料', nameEn: 'Materials', group: 'cyclical' },
  { etf: 'XLRE', nameZh: '房地產', nameEn: 'Real Estate', group: 'rate_sensitive' },
  { etf: 'XLU', nameZh: '公用事業', nameEn: 'Utilities', group: 'defensive' },
];

const BENCHMARK = 'SPY';

/** Yahoo 限流時的示意持股（僅 SPY；板塊 ETF 仍嘗試即時抓取） */
const TOP_HOLDINGS_FALLBACK = {
  SPY: [
    { symbol: 'NVDA', name: 'NVIDIA Corp', pct: 7.85, pctFmt: '7.85%' },
    { symbol: 'AAPL', name: 'Apple Inc', pct: 6.45, pctFmt: '6.45%' },
    { symbol: 'MSFT', name: 'Microsoft Corp', pct: 4.9, pctFmt: '4.90%' },
    { symbol: 'AMZN', name: 'Amazon.com Inc', pct: 3.8, pctFmt: '3.80%' },
    { symbol: 'META', name: 'Meta Platforms Inc', pct: 3.2, pctFmt: '3.20%' },
    { symbol: 'GOOGL', name: 'Alphabet Inc Class A', pct: 2.9, pctFmt: '2.90%' },
    { symbol: 'GOOG', name: 'Alphabet Inc Class C', pct: 2.5, pctFmt: '2.50%' },
    { symbol: 'BRK-B', name: 'Berkshire Hathaway Inc Class B', pct: 2.4, pctFmt: '2.40%' },
    { symbol: 'AVGO', name: 'Broadcom Inc', pct: 2.3, pctFmt: '2.30%' },
    { symbol: 'TSLA', name: 'Tesla Inc', pct: 2.1, pctFmt: '2.10%' },
  ],
};

function closeAt(points, idx) {
  if (!points?.length) return null;
  const i = idx < 0 ? points.length + idx : idx;
  const p = points[i];
  if (!p) return null;
  return p.adjclose ?? p.close;
}

function returnOver(points, days) {
  if (!points?.length || points.length <= days) return null;
  const last = closeAt(points, -1);
  const prev = closeAt(points, -1 - days);
  if (last == null || prev == null || !prev) return null;
  return ((last / prev) - 1) * 100;
}

function avgVolume(points, days) {
  const slice = points.slice(-days).map(p => p.volume).filter(v => v != null && v > 0);
  if (!slice.length) return null;
  return slice.reduce((a, b) => a + b, 0) / slice.length;
}

async function fetchEtfTopHoldings(symbols, limit = 10) {
  const out = {};
  for (const sym of symbols) {
    try {
      const r = await yahooQuoteSummary(sym, 'topHoldings');
      const list = r?.topHoldings?.holdings || [];
      out[sym] = list.slice(0, limit).map(h => ({
        symbol: (h.symbol || '').toUpperCase(),
        name: h.holdingName || h.symbol,
        pct: h.holdingPercent?.raw != null ? h.holdingPercent.raw * 100 : null,
        pctFmt: h.holdingPercent?.fmt || null,
      })).filter(h => h.symbol);
    } catch { /* skip */ }
    await new Promise(r => setTimeout(r, 120));
  }
  return out;
}

function mergeHoldingsWithFallback(fetched, symbols) {
  const out = { ...fetched };
  let usedFallback = false;
  for (const sym of symbols) {
    if (out[sym]?.length) continue;
    if (TOP_HOLDINGS_FALLBACK[sym]) {
      out[sym] = TOP_HOLDINGS_FALLBACK[sym];
      usedFallback = true;
    }
  }
  return { out, usedFallback };
}

async function fetchEtfAum(symbols) {
  const out = {};
  for (const sym of symbols) {
    try {
      const r = await yahooQuoteSummary(sym, 'defaultKeyStatistics');
      const raw = r?.defaultKeyStatistics?.totalAssets?.raw;
      if (raw != null) out[sym] = raw;
    } catch { /* skip symbol */ }
    await new Promise(r => setTimeout(r, 100));
  }
  return out;
}

function rotationQuadrant(rs20, momentum) {
  // 類 RRG：X=相對大盤強度，Y=短期動能（5日減20日）
  if (rs20 >= 0 && momentum >= 0) return { key: 'leading', labelZh: '領漲', tone: 'good' };
  if (rs20 >= 0 && momentum < 0) return { key: 'weakening', labelZh: '轉弱', tone: 'warn' };
  if (rs20 < 0 && momentum >= 0) return { key: 'improving', labelZh: '改善', tone: 'good' };
  return { key: 'lagging', labelZh: '落後', tone: 'bad' };
}

function buildSectorRow(meta, points, spyPoints, aum) {
  const ret1d = returnOver(points, 1);
  const ret5d = returnOver(points, 5);
  const ret20d = returnOver(points, 20);
  const ret60d = returnOver(points, 60);
  const spy20 = returnOver(spyPoints, 20);
  const spy5 = returnOver(spyPoints, 5);
  const rs20 = ret20d != null && spy20 != null ? ret20d - spy20 : null;
  const rs5 = ret5d != null && spy5 != null ? ret5d - spy5 : null;
  const momentum = rs5 != null && rs20 != null ? rs5 - rs20 : null;
  const volToday = points.at(-1)?.volume;
  const avgVol = avgVolume(points, 20);
  const volRatio = volToday && avgVol ? volToday / avgVol : null;
  const flowScore = volRatio != null && ret5d != null
    ? volRatio * (ret5d >= 0 ? 1 : -1) * Math.min(Math.abs(ret5d), 8)
    : null;
  const quad = rs20 != null && momentum != null ? rotationQuadrant(rs20, momentum) : null;
  const price = closeAt(points, -1);
  return {
    ...meta,
    price,
    ret1d, ret5d, ret20d, ret60d,
    rs20, rs5, momentum,
    volRatio,
    flowScore,
    aum,
    aumB: aum != null ? aum / 1e9 : null,
    quadrant: quad,
  };
}

function summarizeRotation(rows) {
  const ranked = [...rows].filter(r => r.rs20 != null).sort((a, b) => b.rs20 - a.rs20);
  const leader = ranked[0];
  const laggard = ranked[ranked.length - 1];
  const byQuad = { leading: [], weakening: [], improving: [], lagging: [] };
  for (const r of rows) {
    if (r.quadrant?.key) byQuad[r.quadrant.key].push(r.etf);
  }
  const cyclicalAvg = avgOf(rows.filter(r => r.group === 'cyclical' || r.group === 'growth'), 'rs20');
  const defensiveAvg = avgOf(rows.filter(r => r.group === 'defensive' || r.group === 'rate_sensitive'), 'rs20');
  let regime = '均衡輪動';
  let regimeNote = '景氣敏感與防禦板塊表現接近，資金未明顯單邊押注。';
  if (cyclicalAvg != null && defensiveAvg != null) {
    const spread = cyclicalAvg - defensiveAvg;
    if (spread > 1.5) {
      regime = '偏景氣／成長';
      regimeNote = `循環型板塊 20 日相對強度平均較防禦型高 ${spread.toFixed(1)} 個百分點，資金偏向風險與景氣復甦敘事。`;
    } else if (spread < -1.5) {
      regime = '偏防禦';
      regimeNote = `防禦型板塊相對較強（差距約 ${Math.abs(spread).toFixed(1)} pct），市場偏避險或降風險偏好。`;
    }
  }
  return {
    leader: leader ? { etf: leader.etf, nameZh: leader.nameZh, rs20: leader.rs20 } : null,
    laggard: laggard ? { etf: laggard.etf, nameZh: laggard.nameZh, rs20: laggard.rs20 } : null,
    ranked: ranked.map(r => ({ etf: r.etf, nameZh: r.nameZh, rs20: r.rs20, quadrant: r.quadrant?.labelZh })),
    byQuadrant: byQuad,
    regime,
    regimeNote,
    cyclicalAvg,
    defensiveAvg,
  };
}

function avgOf(rows, field) {
  const vals = rows.map(r => r[field]).filter(v => v != null);
  if (!vals.length) return null;
  return vals.reduce((a, b) => a + b, 0) / vals.length;
}

function institutionalView(rows) {
  let withAum = rows.filter(r => r.aumB != null).sort((a, b) => b.aumB - a.aumB);
  let aumProxy = false;
  if (!withAum.length) {
    aumProxy = true;
    withAum = [...rows]
      .filter(r => r.price != null)
      .sort((a, b) => (b.flowScore || 0) - (a.flowScore || 0))
      .map((r, i, arr) => {
        const w = Math.max(1, Math.abs(r.flowScore || 0) + Math.abs(r.rs20 || 0) + 1);
        return { etf: r.etf, nameZh: r.nameZh, aumB: w, sharePct: null, _rank: i };
      });
    const sum = withAum.reduce((s, r) => s + r.aumB, 0);
    withAum = withAum.map(r => ({ ...r, sharePct: sum ? (r.aumB / sum) * 100 : null }));
  }
  const totalAum = withAum.reduce((s, r) => s + (r.aumB || 0), 0);
  const byFlow = [...rows].filter(r => r.flowScore != null).sort((a, b) => b.flowScore - a.flowScore);
  return {
    totalAumB: totalAum || null,
    aumProxy,
    byAum: withAum.map(r => ({
      etf: r.etf,
      nameZh: r.nameZh,
      aumB: r.aumB,
      sharePct: r.sharePct ?? (totalAum ? (r.aumB / totalAum) * 100 : null),
    })),
    flowLeaders: byFlow.slice(0, 5).map(r => ({
      etf: r.etf,
      nameZh: r.nameZh,
      flowScore: r.flowScore,
      ret5d: r.ret5d,
      volRatio: r.volRatio,
      note: flowNote(r),
    })),
    flowLaggards: byFlow.slice(-3).reverse().map(r => ({
      etf: r.etf,
      nameZh: r.nameZh,
      flowScore: r.flowScore,
      ret5d: r.ret5d,
      volRatio: r.volRatio,
      note: flowNote(r),
    })),
    disclaimer: aumProxy
      ? 'ETF 總資產暫時無法連線取得，下表改以「流向動能分數」相對占比示意機構資金關注度（非實際 AUM）。流向分數＝量能異常×5日報酬方向。'
      : 'ETF 總資產為被動／機構配置規模 proxy（Yahoo totalAssets）；流向分數結合近 5 日報酬與成交量異常，非官方申報流向。',
  };
}

function flowNote(r) {
  const parts = [];
  if (r.ret5d != null) parts.push(`5日 ${r.ret5d >= 0 ? '+' : ''}${r.ret5d.toFixed(1)}%`);
  if (r.volRatio != null) parts.push(`量能 ${r.volRatio.toFixed(2)}× 均量`);
  return parts.join(' · ');
}

function buildStockExposure(holdingsByEtf, rotation, rows) {
  const packs = [];
  const spy = holdingsByEtf[BENCHMARK];
  if (spy?.length) {
    packs.push({
      etf: BENCHMARK,
      nameZh: 'S&P 500 大盤',
      reason: '指數與被動基金的核心配置，代表整體機構底倉。',
      holdings: spy,
    });
  }
  const ranked = (rotation?.ranked || []).slice(0, 3);
  for (const r of ranked) {
    const list = holdingsByEtf[r.etf];
    if (!list?.length) continue;
    const meta = rows.find(x => x.etf === r.etf);
    packs.push({
      etf: r.etf,
      nameZh: meta?.nameZh || r.nameZh,
      reason: `20 日相對大盤 RS ${r.rs20 != null ? (r.rs20 >= 0 ? '+' : '') + r.rs20.toFixed(1) + '%' : '—'}，資金輪動偏強的板塊。`,
      holdings: list,
    });
  }
  const composite = {};
  for (const p of packs) {
    const w = p.etf === BENCHMARK ? 1 : 0.65;
    for (const h of p.holdings) {
      if (!composite[h.symbol]) composite[h.symbol] = { symbol: h.symbol, name: h.name, score: 0, refs: [] };
      composite[h.symbol].score += (h.pct || 0) * w;
      composite[h.symbol].refs.push(p.etf);
    }
  }
  const topStocks = Object.values(composite)
    .sort((a, b) => b.score - a.score)
    .slice(0, 12)
    .map(s => ({ ...s, refs: [...new Set(s.refs)] }));
  return {
    packs,
    topStocks,
    howToRead: '下方為各 ETF 最新公布的前十大持股（非即時買賣紀錄）。機構「買什麼」在實務上常透過 ETF 與指數基金間接持有；要看單一對沖基金最新建倉，需查 13F（季報、約延遲 45 天）。',
    disclaimer: '持股來自 Yahoo ETF topHoldings，更新頻率通常為每月；與 13F、Dark pool 即時流向不同。',
    usedFallback: false,
  };
}

export async function buildSectorFlowPayload() {
  const symbols = [...SECTOR_ETFS.map(s => s.etf), BENCHMARK];

  // 最先抓大盤持股（避免後續 Yahoo 請求過多被限流）
  let earlyHoldings = {};
  try {
    resetYahooAuth();
    earlyHoldings = await fetchEtfTopHoldings([BENCHMARK], 10);
  } catch { /* optional */ }

  const histories = await Promise.all(symbols.map(async sym => {
    try {
      const h = await getHistory(sym, '6mo', '1d');
      return [sym, h.points];
    } catch {
      return [sym, null];
    }
  }));
  const pts = Object.fromEntries(histories);
  const spyPoints = pts[BENCHMARK];
  if (!spyPoints?.length) throw new Error('無法取得 SPY 基準');

  // 先算輪動，優先抓 ETF 持股（僅 4 檔、避免在 11 次 AUM 之後被 Yahoo 限流）
  const prelimRows = SECTOR_ETFS.map(meta => {
    const points = pts[meta.etf];
    if (!points?.length) return { ...meta, error: 'no_data' };
    return buildSectorRow(meta, points, spyPoints, null);
  }).filter(Boolean);
  const rotationPre = summarizeRotation(prelimRows.filter(s => !s.error));
  const holdingEtfs = [BENCHMARK, ...(rotationPre.ranked || []).slice(0, 3).map(r => r.etf)];
  const uniqueHoldEtfs = [...new Set(holdingEtfs)];
  let stockExposure = null;
  let holdingsUsedFallback = false;
  try {
    let holdingsByEtf = { ...earlyHoldings };
    const needFetch = uniqueHoldEtfs.filter(s => !holdingsByEtf[s]);
    if (needFetch.length) {
      await yahooSleep(300);
      const more = await fetchEtfTopHoldings(needFetch, 10);
      holdingsByEtf = { ...holdingsByEtf, ...more };
    }
    const merged = mergeHoldingsWithFallback(holdingsByEtf, uniqueHoldEtfs);
    holdingsByEtf = merged.out;
    holdingsUsedFallback = merged.usedFallback;
    if (Object.keys(holdingsByEtf).length) {
      stockExposure = buildStockExposure(holdingsByEtf, rotationPre, prelimRows.filter(s => !s.error));
      if (stockExposure && holdingsUsedFallback) {
        stockExposure.disclaimer += ' SPY 持股在資料源限流時暫用近期常見權重示意。';
        stockExposure.usedFallback = true;
      }
    }
  } catch (err) {
    console.warn('[sector-flow] topHoldings:', err?.message || err);
  }

  let aumMap = {};
  try {
    await yahooSleep(400);
    aumMap = await fetchEtfAum(SECTOR_ETFS.map(s => s.etf));
  } catch { /* AUM 可缺 */ }

  const sectors = SECTOR_ETFS.map(meta => {
    const points = pts[meta.etf];
    if (!points?.length) return { ...meta, error: 'no_data' };
    return buildSectorRow(meta, points, spyPoints, aumMap[meta.etf] ?? null);
  }).filter(Boolean);

  const rotation = summarizeRotation(sectors.filter(s => !s.error));
  const okRows = sectors.filter(s => !s.error);
  const institutional = institutionalView(okRows);
  if (stockExposure && rotation?.leader) {
    stockExposure = buildStockExposure(
      Object.fromEntries((stockExposure.packs || []).map(p => [p.etf, p.holdings])),
      rotation,
      okRows,
    );
  }

  return {
    updatedAt: new Date().toISOString(),
    benchmark: BENCHMARK,
    source: 'Yahoo Finance · SPDR 行業 ETF',
    sectors,
    rotation,
    institutional,
    stockExposure,
    heatmapWindow: { d1: '1日', d5: '5日', d20: '20日', d60: '約60日' },
  };
}