// 日曆資料庫快取：以「日」為單位，預設 24 小時內不重抓；重啟 server 仍保留
import { buildCalendar } from './calendar.js';
import { localizeCalendarPayload } from './calendar-i18n.js';
import { getCachedEntry, putCachedJSON } from './db.js';

export const CALENDAR_DAY_MS = (Number(process.env.CALENDAR_TTL_HOURS) || 24) * 3600 * 1000;

export function calendarCacheDay(d = new Date()) {
  return d.toISOString().slice(0, 10);
}

function baseKey(day) {
  return `calendar:base:v5:${day}`;
}

function earnKey(day, symbols) {
  return `calendar:earn:v5:${day}:${[...symbols].sort().join(',') || '_'}`;
}

function watchlistKey() {
  return 'calendar:watchlist:v1';
}

export function getCalendarWatchlist() {
  const row = getCachedEntry(watchlistKey());
  const val = row?.value;
  return Array.isArray(val) ? val : [];
}

export function saveCalendarWatchlist(symbols) {
  const clean = [...new Set((symbols || []).map(s => String(s).trim().toUpperCase()).filter(Boolean))].slice(0, 30);
  putCachedJSON(watchlistKey(), clean);
  return clean;
}

function isFresh(entry, day) {
  if (!entry?.value) return false;
  if (entry.value.cacheDay && entry.value.cacheDay !== day) return false;
  return Date.now() - entry.updatedAt < CALENDAR_DAY_MS;
}

function mergeEvents(baseEvents, earnEvents) {
  const seen = new Set();
  return [...(baseEvents || []), ...(earnEvents || [])]
    .filter(ev => {
      const key = `${ev.date}|${ev.time}|${ev.category}|${ev.symbol || ''}|${ev.title}`;
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    })
    .sort((a, b) => (a.date + a.time).localeCompare(b.date + b.time));
}

function filterRange(events, start, end) {
  return (events || []).filter(ev => ev.date >= start && ev.date <= end);
}

function wrapResponse(payload, entry, { cached, stale, fetchError } = {}) {
  return {
    ...payload,
    cached: !!cached,
    stale: !!stale,
    fetchError: fetchError || null,
    cachedAt: entry?.updatedAt ? new Date(entry.updatedAt).toISOString() : payload.updatedAt,
    cacheDay: payload.cacheDay,
  };
}

async function buildAndStore({ start, end, symbols, key, day }) {
  const payload = await buildCalendar({ start, end, symbols });
  const localized = await localizeCalendarPayload(payload);
  localized.cacheDay = day;
  localized.cachedAt = new Date().toISOString();
  putCachedJSON(key, localized);
  return { value: localized, updatedAt: Date.now() };
}

export async function getCalendarPayload({ start, end, symbols, forceFresh = false }) {
  const day = calendarCacheDay();
  const sym = [...symbols].sort();
  let baseEntry = getCachedEntry(baseKey(day));
  let earnEntry = sym.length ? getCachedEntry(earnKey(day, sym)) : null;

  const needBase = forceFresh || !isFresh(baseEntry, day);
  const needEarn = sym.length > 0 && (forceFresh || !isFresh(earnEntry, day));

  try {
    if (needBase) {
      baseEntry = await buildAndStore({ start, end, symbols: [], key: baseKey(day), day });
    }
    if (needEarn) {
      earnEntry = await buildAndStore({ start, end, symbols: sym, key: earnKey(day, sym), day });
    }

    const baseEvents = (baseEntry.value.events || []).filter(e => e.category !== 'earnings');
    const fullEarn = earnEntry?.value?.events || [];
    const earnEvents = sym.length ? fullEarn.filter(e => e.category === 'earnings') : [];
    const events = filterRange(mergeEvents(baseEvents, earnEvents), start, end);

    const sources = baseEntry.value.sources || [];
    const payload = {
      updatedAt: new Date().toISOString(),
      cacheDay: day,
      start,
      end,
      symbols: sym,
      events,
      sources,
    };
    return wrapResponse(payload, baseEntry, { cached: !needBase && !needEarn });
  } catch (err) {
    const staleBase = baseEntry?.value;
    const staleEarn = earnEntry?.value;
    if (staleBase) {
      const baseEvents = (staleBase.events || []).filter(e => e.category !== 'earnings');
      const earnEvents = sym.length ? (staleEarn?.events || []).filter(e => e.category === 'earnings') : [];
      const events = filterRange(mergeEvents(baseEvents, earnEvents), start, end);
      return wrapResponse({
        updatedAt: staleBase.updatedAt,
        cacheDay: staleBase.cacheDay || day,
        start,
        end,
        symbols: sym,
        events,
        sources: staleBase.sources || [],
      }, baseEntry, { cached: true, stale: true, fetchError: String(err?.message || err) });
    }
    throw err;
  }
}

export async function warmCalendarCache() {
  const today = calendarCacheDay();
  const start = today;
  const end = addDaysISO(today, 60);
  if (isFresh(getCachedEntry(baseKey(today)), today)) return;
  await buildAndStore({ start, end, symbols: [], key: baseKey(today), day: today });
}

function addDaysISO(iso, days) {
  const d = new Date(iso + 'T00:00:00Z');
  d.setUTCDate(d.getUTCDate() + days);
  return d.toISOString().slice(0, 10);
}
