// 個股 OHLCV：以 SQLite price_bars 為準，Yahoo/Nasdaq 只補「還沒有的」K 線
import {
  upsertPriceBars, getPriceBars, getPriceBarMeta, priceBarsToPoints, deletePriceBars,
  getCachedEntry, putCachedJSON,
} from './db.js';
import { getHistory, getHistorySince } from './marketdata.js';

const META_PREFIX = 'histmeta:';

function metaKey(symbol, interval) {
  return `${META_PREFIX}${symbol}:${interval}`;
}

function barPointCount(bars) {
  return bars?.length || 0;
}

/** 從舊版 JSON 快取（hist:SYM:range:1d）匯入 DB，避免重打 API */
export function importLegacyHistCaches(symbol, interval = '1d') {
  const keys = [
    `hist:${symbol}:max:${interval}`,
    `hist:${symbol}:10y:${interval}`,
    `hist:${symbol}:5y:${interval}`,
    `hist:${symbol}:2y:${interval}`,
  ];
  let best = null;
  for (const key of keys) {
    const entry = getCachedEntry(key);
    const pts = entry?.value?.points;
    if (!pts?.length) continue;
    if (!best || pts.length > best.points.length) best = { key, entry, points: pts };
  }
  if (!best) return 0;
  const n = upsertPriceBars(symbol, interval, best.points);
  const v = best.entry.value;
  putCachedJSON(metaKey(symbol, interval), {
    symbol: v.symbol || symbol,
    name: v.name || null,
    currency: v.currency || null,
    source: v.source || 'legacy-cache',
    interval,
    _importedFrom: best.key,
    _fetchedAt: best.entry.updatedAt || Date.now(),
  });
  return n;
}

function todayISO() {
  return new Date().toISOString().slice(0, 10);
}

function startOfWeekISO(dateStr) {
  const d = new Date(dateStr + 'T12:00:00Z');
  const diff = (d.getUTCDay() + 6) % 7;
  d.setUTCDate(d.getUTCDate() - diff);
  return d.toISOString().slice(0, 10);
}

/** 非即時研究：去掉可能尚未收盤的當根 K（日線≤昨日、周線≤上週、月線≤上月） */
export function filterResearchBars(points, interval = '1d') {
  if (!points?.length) return [];
  const today = todayISO();
  if (interval === '1d') {
    return points.filter(p => p.date < today);
  }
  const last = points[points.length - 1];
  if (interval === '1wk') {
    if (startOfWeekISO(last.date) >= startOfWeekISO(today)) return points.slice(0, -1);
    return points;
  }
  if (interval === '1mo') {
    if (last.date.slice(0, 7) >= today.slice(0, 7)) return points.slice(0, -1);
    return points;
  }
  return points;
}

const TTL_BY_INTERVAL = {
  '1d': 6 * 3600 * 1000,
  '1wk': 24 * 3600 * 1000,
  '1mo': 7 * 24 * 3600 * 1000,
};

function weekKey(dateStr) {
  const d = new Date(dateStr + 'T12:00:00Z');
  const day = d.getUTCDay() || 7;
  d.setUTCDate(d.getUTCDate() + 4 - day);
  const y = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  const w = Math.ceil((((d - y) / 86400000) + 1) / 7);
  return `${d.getUTCFullYear()}-W${String(w).padStart(2, '0')}`;
}

function resampleBars(dailyPoints, interval) {
  const keyFn = interval === '1wk'
    ? weekKey
    : (dateStr) => dateStr.slice(0, 7);
  const groups = new Map();
  for (const p of dailyPoints) {
    const k = keyFn(p.date);
    if (!groups.has(k)) groups.set(k, []);
    groups.get(k).push(p);
  }
  const out = [];
  for (const arr of [...groups.values()]) {
    arr.sort((a, b) => (a.date < b.date ? -1 : 1));
    const last = arr[arr.length - 1];
    const highs = arr.map(x => x.high ?? x.close);
    const lows = arr.map(x => x.low ?? x.close);
    out.push({
      date: last.date,
      open: arr[0].open ?? arr[0].close,
      high: Math.max(...highs),
      low: Math.min(...lows),
      close: last.close,
      volume: arr.some(x => x.volume != null) ? arr.reduce((s, x) => s + (x.volume || 0), 0) : null,
      adjclose: last.adjclose ?? last.close,
    });
  }
  return out.sort((a, b) => (a.date < b.date ? -1 : 1));
}

async function fetchOrResample(symbol, interval) {
  if (interval === '1d') return getHistory(symbol, 'max', '1d');
  try {
    const hist = await getHistory(symbol, 'max', interval);
    if (hist.points?.length >= 2 && barsMatchInterval(
      hist.points.map(p => ({ date: p.date })),
      interval,
    )) return hist;
  } catch (_) { /* Yahoo 429 等 → 改本機重採樣 */ }
  await ensurePriceHistory(symbol, '1d', { fresh: false });
  const daily = priceBarsToPoints(getPriceBars(symbol, '1d'));
  if (daily.length < 40) throw new Error('日線不足，無法合成周/月線');
  const points = resampleBars(daily, interval);
  if (points.length < 2) throw new Error('重採樣後資料過少');
  return {
    symbol,
    name: null,
    currency: null,
    range: 'max',
    interval,
    source: interval === '1wk' ? '本機日線→周線' : '本機日線→月線',
    points,
  };
}

/** 偵測 DB 是否誤存日線到周/月線 */
function barsMatchInterval(bars, interval) {
  if (!bars?.length || interval === '1d') return true;
  if (interval === '1wk' && bars.length > 600) return false;
  if (interval === '1mo' && bars.length > 200) return false;
  if (bars.length < 3) return true;
  const i = bars.length - 1;
  const gap = (new Date(bars[i].date) - new Date(bars[i - 1].date)) / 86400000;
  if (interval === '1wk') return gap >= 4;
  if (interval === '1mo') return gap >= 20;
  return true;
}

function planFetch(bars, metaUpdatedAt, fresh, ttlMs, interval) {
  if (!bars.length) return 'full';
  if (fresh) return 'incremental';
  const research = filterResearchBars(bars.map(b => ({ date: b.date })), interval);
  const lastComplete = research.length ? research[research.length - 1].date : null;
  const lastStored = bars[bars.length - 1].date;
  const age = Date.now() - (metaUpdatedAt || 0);
  if (lastComplete && lastStored > lastComplete && age > ttlMs / 2) return 'incremental';
  if (lastComplete && lastComplete < todayISO() && age > ttlMs) return 'incremental';
  if (age > ttlMs * 14) return 'incremental';
  return null;
}

/**
 * @returns {{ payload: object, cached: boolean, fetchMode: string|null }}
 */
export async function ensurePriceHistory(symbol, interval = '1d', { fresh = false, ttlMs } = {}) {
  if (!ttlMs) ttlMs = TTL_BY_INTERVAL[interval] || TTL_BY_INTERVAL['1d'];
  let bars = getPriceBars(symbol, interval);
  if (!bars.length) importLegacyHistCaches(symbol, interval);
  bars = getPriceBars(symbol, interval);
  if (bars.length && !barsMatchInterval(bars, interval)) {
    deletePriceBars(symbol, interval);
    bars = [];
  }

  const mk = metaKey(symbol, interval);
  let metaEntry = getCachedEntry(mk);
  let meta = metaEntry?.value || { symbol, interval };

  const mode = planFetch(bars, metaEntry?.updatedAt, fresh, ttlMs, interval);
  let fetchError = null;

  if (mode === 'full') {
    try {
      const hist = await fetchOrResample(symbol, interval);
      upsertPriceBars(symbol, interval, hist.points);
      meta = {
        symbol: hist.symbol || symbol,
        name: hist.name,
        currency: hist.currency,
        source: hist.source,
        interval,
        _fetchedAt: Date.now(),
        _fetchMode: 'full',
      };
      putCachedJSON(mk, meta);
      bars = getPriceBars(symbol, interval);
    } catch (e) {
      fetchError = String(e?.message || e);
      if (!bars.length) throw e;
    }
  } else if (mode === 'incremental') {
    const lastDate = bars[bars.length - 1].date;
    try {
      let patch;
      try {
        patch = await getHistorySince(symbol, lastDate, 'max', interval);
        if (interval !== '1d' && !barsMatchInterval(patch.points.map(p => ({ date: p.date })), interval)) {
          throw new Error('patch_not_weekly');
        }
      } catch {
        const daily = priceBarsToPoints(getPriceBars(symbol, '1d'));
        const resampled = resampleBars(daily, interval);
        const idx = resampled.findIndex(p => p.date > lastDate);
        patch = {
          symbol,
          interval,
          points: idx >= 0 ? resampled.slice(idx) : [],
          source: interval === '1wk' ? '本機日線→周線' : '本機日線→月線',
        };
      }
      const added = upsertPriceBars(symbol, interval, patch.points);
      meta = {
        ...meta,
        symbol: patch.symbol || symbol,
        name: patch.name || meta.name,
        currency: patch.currency || meta.currency,
        source: patch.source || meta.source,
        interval,
        _fetchedAt: Date.now(),
        _fetchMode: 'incremental',
        _lastIncrementalAt: Date.now(),
        _barsAdded: added,
      };
      putCachedJSON(mk, meta);
      bars = getPriceBars(symbol, interval);
    } catch (e) {
      fetchError = String(e?.message || e);
    }
  }

  const stat = getPriceBarMeta(symbol, interval);
  const allPoints = priceBarsToPoints(bars);
  const points = filterResearchBars(allPoints, interval);
  const lastResearch = points.length ? points[points.length - 1].date : null;
  return {
    payload: {
      symbol,
      name: meta.name || null,
      currency: meta.currency || null,
      interval,
      source: meta.source || 'MacroScope DB',
      range: 'max',
      points,
      allBarsPoints: allPoints,
      dbBars: stat.n,
      researchBars: points.length,
      firstDate: points[0]?.date || stat.first_date,
      lastDate: lastResearch || stat.last_date,
      researchThrough: lastResearch,
      researchNote: interval === '1d'
        ? '研究用日線截至昨日完整 K 線（今日未收盤不納入）'
        : interval === '1wk'
          ? '研究用周線截至上一根完整週 K'
          : '研究用月線截至上一根完整月 K',
      cached: mode == null,
      fetchMode: mode,
      fetchError,
    },
    cached: mode == null,
    fetchMode: mode,
  };
}

/** 成交量圖：在研究用 K 線之外，盡量附上「當日」成交量（來自 DB 當根或即時報價） */
export function buildVolumeSeries(researchPoints, allPoints, quote = {}, interval = '1d') {
  if (interval !== '1d' || !researchPoints?.length) return researchPoints || [];
  const today = todayISO();
  let todayVol = quote?.volume != null ? Number(quote.volume) : null;
  const rawToday = (allPoints || []).find(p => p.date === today);
  if (rawToday?.volume != null) todayVol = Number(rawToday.volume);

  const pts = researchPoints.map(p => ({ ...p }));
  if (todayVol == null || isNaN(todayVol)) return pts;

  const last = pts[pts.length - 1];
  if (last?.date === today) {
    pts[pts.length - 1] = { ...last, volume: todayVol };
    return pts;
  }

  const px = quote?.price ?? quote?.regularMarketPrice ?? last?.close;
  if (px == null) return pts;
  pts.push({
    date: today,
    open: quote?.previousClose ?? px,
    high: quote?.dayHigh ?? px,
    low: quote?.dayLow ?? px,
    close: px,
    volume: todayVol,
    adjclose: px,
    partialSession: true,
  });
  return pts;
}