// ═══════════════════════════════════════════════════════════
//  calendar.js — 重大事件日曆（免費/官方來源優先）
//  來源：
//   - Federal Reserve FOMC calendar（利率決議、SEP/點陣圖、會議紀要）
//   - BLS 官方 iCalendar（CPI、就業、PPI、JOLTS 等）
//   - BEA release schedule（GDP、PCE、Personal Income/Outlays）
//   - Nasdaq earnings calendar（追蹤股票財報日）
//   - 四巫日（3/6/9/12 月第三個週五，衍生品結算）
//   - FRED 發布日程（零售、房市、ADP、初領失業金等）
//   - 市場結構日（月選擇權結算、美股休市、Jackson Hole、ECB/BOJ/BOE）
// ═══════════════════════════════════════════════════════════
import { fetchFredMacroEvents } from './calendar-fred.js';
import { fetchMarketStructureEvents } from './calendar-market.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';

const MONTHS = {
  january: 0, jan: 0, february: 1, feb: 1, march: 2, mar: 2, april: 3, apr: 3,
  may: 4, june: 5, jun: 5, july: 6, jul: 6, august: 7, aug: 7,
  september: 8, sep: 8, october: 9, oct: 9, november: 10, nov: 10, december: 11, dec: 11,
};
const IMPACT_WORDS = [
  ['FOMC', 'high'], ['Federal Funds', 'high'], ['Interest Rate', 'high'],
  ['CPI', 'high'], ['Consumer Price Index', 'high'], ['Employment Situation', 'high'],
  ['Nonfarm', 'high'], ['Payroll', 'high'], ['PCE', 'high'], ['GDP', 'high'],
  ['Producer Price', 'medium'], ['PPI', 'medium'], ['Job Openings', 'medium'],
  ['JOLTS', 'medium'], ['Retail Sales', 'medium'], ['Personal Income', 'medium'],
  ['Import and Export Price', 'medium'], ['Productivity and Costs', 'medium'],
  ['Employment Cost Index', 'medium'], ['Real Earnings', 'low'],
];
const BLS_SKIP = /Metropolitan Area|State Employment|State Job Openings|County Employment|American Time Use|Quarterly Data Series on Business Employment Dynamics|Usual Weekly Earnings|State Unemployment \(Monthly\)/i;
const TITLE_MAP = [
  [/Consumer Price Index|CPI/i, 'CPI 通膨'],
  [/Employment Situation|Nonfarm|Payroll/i, '非農就業 / 失業率'],
  [/Producer Price|PPI/i, 'PPI 生產者物價'],
  [/Job Openings|JOLTS/i, 'JOLTS 職缺'],
  [/Import and Export Price/i, '進出口物價'],
  [/Real Earnings/i, '實質薪資'],
  [/GDP/i, 'GDP'],
  [/Personal Income|Personal Outlays|PCE/i, 'PCE / 個人收入支出'],
  [/Import and Export Price/i, '進出口物價'],
  [/Productivity and Costs/i, '生產力 / 單位成本'],
  [/Employment Cost Index/i, '就業成本指數 ECI'],
];
const DEDUP_GROUPS = [
  [/cpi|消費者物價/i, 'cpi'],
  [/非農|employment situation/i, 'nfp'],
  [/ppi|生產者物價/i, 'ppi'],
  [/jolts|職缺/i, 'jolts'],
  [/gdp|國內生產/i, 'gdp'],
  [/pce|個人收入支出/i, 'pce'],
  [/fomc.*紀要|會議紀要|minutes/i, 'fomc_min'],
  [/fomc|利率決議|sep|點陣/i, 'fomc'],
  [/四巫/i, 'quad_witch'],
  [/月選擇權結算/i, 'monthly_opex'],
  [/就業成本|employment cost index|\beci\b/i, 'eci'],
  [/生產力|productivity and costs/i, 'productivity'],
  [/國際貿易|international trade/i, 'trade'],
  [/新屋開工|營建許可|housing starts|building permits/i, 'housing'],
  [/零售銷售|retail sales/i, 'retail'],
  [/工業生產|industrial production/i, 'indpro'],
  [/密西根|consumer sentiment|surveys of consumers/i, 'umich'],
  [/初領失業|jobless claims/i, 'claims'],
  [/adp/i, 'adp'],
  [/ec\s*利率|ecb/i, 'ecb'],
  [/日本央行|boj/i, 'boj'],
  [/英央行|mpc/i, 'boe'],
];
const IMPACT_RANK = { high: 0, medium: 1, low: 2 };

async function text(url, headers = {}, ms = 12000) {
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), ms);
  try {
    const res = await fetch(url, { headers: { 'User-Agent': UA, Accept: '*/*', ...headers }, signal: ctrl.signal });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.text();
  } finally { clearTimeout(timer); }
}
async function json(url, headers = {}, ms = 12000) {
  return JSON.parse(await text(url, { Accept: 'application/json,text/plain,*/*', ...headers }, ms));
}
function iso(y, m, d) {
  return new Date(Date.UTC(y, m, d)).toISOString().slice(0, 10);
}
function inRange(date, start, end) {
  return date >= start && date <= end;
}
function impact(title) {
  for (const [word, level] of IMPACT_WORDS) if (title.toLowerCase().includes(word.toLowerCase())) return level;
  return 'low';
}
function shortTitle(title) {
  for (const [re, label] of TITLE_MAP) if (re.test(title)) return label;
  return String(title || '').replace(/^U\.S\.\s*/i, '').replace(/\s+News Release.*$/i, '').trim();
}
function addEvent(events, ev) {
  if (!ev?.date || !ev?.title) return;
  events.push({
    time: ev.time || '',
    impact: ev.impact || impact(ev.title),
    category: ev.category || 'macro',
    source: ev.source || '',
    symbol: ev.symbol || null,
    url: ev.url || null,
    note: ev.note || '',
    ...ev,
  });
}

function parseICalDate(raw) {
  const m = String(raw || '').match(/(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2}))?/);
  if (!m) return null;
  return { date: `${m[1]}-${m[2]}-${m[3]}`, time: m[4] ? `${m[4]}:${m[5]}` : '' };
}
function unfoldICS(src) {
  return src.replace(/\r\n[ \t]/g, '').replace(/\n[ \t]/g, '');
}
function parseICS(src) {
  const blocks = unfoldICS(src).split('BEGIN:VEVENT').slice(1);
  return blocks.map(block => {
    const get = (name) => {
      const re = new RegExp(`^${name}(?:;[^:]*)?:(.*)$`, 'mi');
      return block.match(re)?.[1]?.trim() || '';
    };
    const dt = parseICalDate(get('DTSTART'));
    return dt ? { ...dt, title: get('SUMMARY'), description: get('DESCRIPTION'), url: get('URL') } : null;
  }).filter(Boolean);
}

export async function fetchBlsEvents(start, end) {
  const events = [];
  const ics = await text('https://www.bls.gov/schedule/news_release/bls.ics');
  for (const item of parseICS(ics)) {
    if (!inRange(item.date, start, end)) continue;
    const title = item.title || '';
    if (BLS_SKIP.test(title)) continue;
    if (impact(title) === 'low' && !/Real Earnings|Import and Export Price/i.test(title)) continue;
    addEvent(events, {
      date: item.date,
      time: item.time || '08:30',
      title: shortTitle(title),
      category: 'macro',
      impact: impact(title),
      source: 'BLS',
      url: item.url || 'https://www.bls.gov/schedule/news_release/',
      note: shortTitle(title),
    });
  }
  return events;
}

export async function fetchBeaEvents(start, end) {
  const events = [];
  const html = await text('https://www.bea.gov/news/schedule/full');
  const rowRe = /<tr[\s\S]*?<\/tr>/gi;
  for (const row of html.match(rowRe) || []) {
    const clean = row.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
    const m = clean.match(/\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2})\s+(\d{1,2}:\d{2}\s+[AP]M)?\s*(.*)/i);
    if (!m) continue;
    const year = Number((html.match(/<option[^>]*selected[^>]*>\s*(20\d{2})\s*</i) || [])[1]) || new Date().getUTCFullYear();
    const date = iso(year, MONTHS[m[1].toLowerCase()], Number(m[2]));
    if (!inRange(date, start, end)) continue;
    const rawTitle = (m[4] || '').replace(/\bN\s*ews\b/i, '').trim();
    if (!/GDP|Personal Income|Personal Outlays|PCE|International Trade|Corporate Profits/i.test(rawTitle)) continue;
    addEvent(events, {
      date,
      time: m[3] || '08:30 AM',
      title: shortTitle(rawTitle),
      category: 'macro',
      impact: impact(rawTitle),
      source: 'BEA',
      url: 'https://www.bea.gov/news/schedule',
      note: shortTitle(rawTitle),
    });
  }
  return events;
}

export async function fetchFomcEvents(start, end) {
  const events = [];
  const html = await text('https://www.federalreserve.gov/monetarypolicy/fomccalendars.htm');
  const yearRe = /<h4[^>]*>\s*<a[^>]*>(20\d{2}) FOMC Meetings<\/a><\/h4>([\s\S]*?)(?=<h4[^>]*>\s*<a[^>]*>20\d{2} FOMC Meetings|$)/gi;
  let yMatch;
  while ((yMatch = yearRe.exec(html))) {
    const year = Number(yMatch[1]);
    const section = yMatch[2];
    const chunks = section.split(/(?=<div[^>]*fomc-meeting__month)/i).slice(1);
    for (const row of chunks) {
      const month = row.match(/fomc-meeting__month[\s\S]*?<strong>([^<]+)<\/strong>/i)?.[1]?.trim();
      const dateText = row.match(/fomc-meeting__date[^>]*>([^<]+)<\/div>/i)?.[1]?.trim();
      if (!month || !dateText) continue;
      const monthIndex = MONTHS[month.toLowerCase()];
      if (monthIndex == null) continue;
      const dayNums = dateText.match(/\d{1,2}/g)?.map(Number) || [];
      if (!dayNums.length) continue;
      const date = iso(year, monthIndex, dayNums[dayNums.length - 1]);
      if (inRange(date, start, end)) {
        const hasSep = /\*/.test(dateText) || /Projection|projections|SEP/i.test(row);
        addEvent(events, {
          date,
          time: '14:00 ET',
          title: hasSep ? 'FOMC 利率決議 + SEP 點陣圖' : 'FOMC 利率決議',
          category: 'fed',
          impact: 'high',
          source: 'Federal Reserve',
          url: 'https://www.federalreserve.gov/monetarypolicy/fomccalendars.htm',
          note: hasSep ? '含 Summary of Economic Projections / dot plot' : '政策聲明與利率決議',
        });
      }
      const min = row.match(/Released\s+([A-Za-z]+)\s+(\d{1,2}),\s+(20\d{2})/i);
      if (min) {
        const minDate = iso(Number(min[3]), MONTHS[min[1].toLowerCase()], Number(min[2]));
        if (inRange(minDate, start, end)) addEvent(events, {
          date: minDate,
          time: '14:00 ET',
          title: 'FOMC 會議紀要',
          category: 'fed',
          impact: 'medium',
          source: 'Federal Reserve',
          url: 'https://www.federalreserve.gov/monetarypolicy/fomccalendars.htm',
          note: `${year} 年 ${month} FOMC 會議紀要`,
        });
      }
    }
  }
  return events;
}

function daysBetween(start, end) {
  const out = [];
  const d = new Date(start + 'T00:00:00Z');
  const e = new Date(end + 'T00:00:00Z');
  while (d <= e) {
    out.push(d.toISOString().slice(0, 10));
    d.setUTCDate(d.getUTCDate() + 1);
  }
  return out;
}
function nasdaqDate(isoDate) {
  const d = new Date(isoDate + 'T00:00:00Z');
  return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
}

const QUAD_WITCH_MONTHS = [2, 5, 8, 11]; // 3、6、9、12 月

function thirdFridayISO(year, monthIndex) {
  const dow = new Date(Date.UTC(year, monthIndex, 1)).getUTCDay();
  const day = 1 + ((5 - dow + 7) % 7) + 14;
  return iso(year, monthIndex, day);
}

export function fetchQuadrupleWitchingEvents(start, end) {
  const events = [];
  const y0 = Number(start.slice(0, 4));
  const y1 = Number(end.slice(0, 4));
  for (let y = y0; y <= y1; y++) {
    for (const mo of QUAD_WITCH_MONTHS) {
      const date = thirdFridayISO(y, mo);
      if (!inRange(date, start, end)) continue;
      addEvent(events, {
        date,
        time: '16:00 美東',
        title: '四巫日（衍生品結算）',
        category: 'derivatives',
        impact: 'high',
        source: 'Market calendar',
        note: '每季第三個週五，期指、指數選擇權、個股選擇權等同日到期，成交與波動常明顯放大',
      });
    }
  }
  return events;
}

function dedupeKey(ev) {
  const title = String(ev.title || '');
  const text = `${title} ${ev.note || ''}`;
  for (const [re, slug] of DEDUP_GROUPS) {
    if (re.test(title)) return `${ev.date}|${slug}`;
  }
  for (const [re, slug] of DEDUP_GROUPS) {
    if (re.test(text)) return `${ev.date}|${slug}`;
  }
  return `${ev.date}|${ev.time}|${ev.category}|${ev.symbol || ''}|${ev.title}`;
}
function pickBetterEvent(a, b) {
  const ra = IMPACT_RANK[a.impact] ?? 2;
  const rb = IMPACT_RANK[b.impact] ?? 2;
  if (ra !== rb) return ra < rb ? a : b;
  return (a.title?.length || 0) >= (b.title?.length || 0) ? a : b;
}
function dedupeEvents(events) {
  const byKey = new Map();
  for (const ev of events) {
    const key = dedupeKey(ev);
    const prev = byKey.get(key);
    byKey.set(key, prev ? pickBetterEvent(prev, ev) : ev);
  }
  return [...byKey.values()];
}

export async function fetchEarningsEvents(start, end, symbols = []) {
  const wanted = new Set((symbols || []).map(s => String(s).trim().toUpperCase()).filter(Boolean));
  if (!wanted.size) return [];
  const events = [];
  const headers = { Accept: 'application/json', Origin: 'https://www.nasdaq.com', Referer: 'https://www.nasdaq.com/market-activity/earnings' };
  const dates = daysBetween(start, end);
  for (let i = 0; i < dates.length; i += 4) {
    const chunk = dates.slice(i, i + 4);
    const results = await Promise.all(chunk.map(date => json(`https://api.nasdaq.com/api/calendar/earnings?date=${nasdaqDate(date)}`, headers).catch(() => null)));
    results.forEach((j, idx) => {
      const date = chunk[idx];
      const rows = j?.data?.rows || [];
      for (const r of rows) {
        const sym = String(r.symbol || '').toUpperCase();
        if (!wanted.has(sym)) continue;
        const name = r.name ? String(r.name).trim() : sym;
        const bits = [name !== sym ? name : ''];
        if (r.epsForecast) bits.push(`EPS 預估 ${r.epsForecast}`);
        if (r.fiscalQuarterEnding) bits.push(`財季 ${r.fiscalQuarterEnding}`);
        addEvent(events, {
          date,
          time: r.time ? String(r.time).replace(/\bET\b/i, '美東').replace(/\bAM\b/i, '上午').replace(/\bPM\b/i, '下午') : '',
          title: `${sym} 財報`,
          symbol: sym,
          category: 'earnings',
          impact: 'high',
          source: 'Nasdaq earnings',
          url: `https://www.nasdaq.com/market-activity/stocks/${sym.toLowerCase()}/earnings`,
          note: bits.filter(Boolean).join(' · ') || `${sym} 財報`,
        });
      }
    });
  }
  return events;
}

export async function buildCalendar({ start, end, symbols }) {
  const sourceResults = await Promise.allSettled([
    fetchFomcEvents(start, end),
    fetchBlsEvents(start, end),
    fetchBeaEvents(start, end),
    fetchEarningsEvents(start, end, symbols),
    Promise.resolve(fetchQuadrupleWitchingEvents(start, end)),
    fetchFredMacroEvents(start, end),
    Promise.resolve(fetchMarketStructureEvents(start, end)),
  ]);
  const events = dedupeEvents(sourceResults.flatMap(r => r.status === 'fulfilled' ? r.value : []))
    .sort((a, b) => (a.date + a.time).localeCompare(b.date + b.time));
  return {
    updatedAt: new Date().toISOString(),
    start, end, symbols,
    events,
    sources: sourceResults.map((r, i) => ({
      name: ['Federal Reserve', 'BLS', 'BEA', 'Nasdaq earnings', 'Market calendar', 'FRED releases', 'Global markets'][i],
      ok: r.status === 'fulfilled',
      error: r.status === 'rejected' ? String(r.reason?.message || r.reason) : null,
    })),
  };
}
