#!/usr/bin/env node

import { loadEnvFile, CHROME_UA, runSeed, writeExtraKey } from './_seed-utils.mjs';
import { tokensToContentMeta, DAY_MIN } from './_content-age-helpers.mjs';

loadEnvFile(import.meta.url);

const BIS_BASE = 'https://stats.bis.org/api/v1/data';
// Content-age budget — the canonical key holds the BIS WS_CBPOL policy-rate
// series (monthly). 75 days clears the monthly publication lag plus a missed
// cycle; STALE_CONTENT fires if BIS stops publishing policy rates. Detects a
// whole-feed freeze (the CISS failure mode, issue #3845); a single-series
// freeze among policy/exchange/credit is not modelled — the canonical key is
// policy only, and exchange/credit are best-effort extra keys.
const BIS_POLICY_MAX_CONTENT_AGE_MIN = 75 * DAY_MIN;

const BIS_COUNTRIES = {
  US: { name: 'United States', centralBank: 'Federal Reserve' },
  GB: { name: 'United Kingdom', centralBank: 'Bank of England' },
  JP: { name: 'Japan', centralBank: 'Bank of Japan' },
  XM: { name: 'Euro Area', centralBank: 'ECB' },
  CH: { name: 'Switzerland', centralBank: 'Swiss National Bank' },
  SG: { name: 'Singapore', centralBank: 'MAS' },
  IN: { name: 'India', centralBank: 'Reserve Bank of India' },
  AU: { name: 'Australia', centralBank: 'RBA' },
  CN: { name: 'China', centralBank: "People's Bank of China" },
  CA: { name: 'Canada', centralBank: 'Bank of Canada' },
  KR: { name: 'South Korea', centralBank: 'Bank of Korea' },
  BR: { name: 'Brazil', centralBank: 'Banco Central do Brasil' },
};

const BIS_COUNTRY_KEYS = Object.keys(BIS_COUNTRIES).join('+');

const KEYS = {
  policy:   'economic:bis:policy:v1',
  exchange: 'economic:bis:eer:v1',
  credit:   'economic:bis:credit:v1',
};

// 36 hours = 3× the bundle's 12h interval gate (seed-bundle-macro.mjs:5,
// `intervalMs: 12 * HOUR`). Per the gold-standard "TTL >= 3× cron interval"
// recipe — earlier TTL=43200 (12h) matched the gate exactly, so any cron
// drift left the canonical key TTL'd-out for a window where /api/health
// reported `economic:bis:policy:v1`/`eer:v1`/`credit:v1` as missing while
// `seed-meta:economic:bis` still carried last-good `recordCount` (verified
// 2026-05-06: seed-meta showed recordCount=11 + a recent fetchedAt, but
// all 3 canonical GETs returned nil from Upstash because the bundle ran
// ~13.7h after the last successful tick instead of exactly 12h). 36h
// covers cron drift + one degraded-to-24h cycle (matches the rationale
// already applied to bisDsr/bisProperty* maxStaleMin in api/health.js
// circa 2026-04-27, just on the canonical-key-TTL side instead of the
// health-threshold side).
//
// All 3 canonical writes (policy via atomicPublish, eer + credit via
// writeExtraKey in afterPublish) reuse this constant, so the bump fixes
// all three simultaneously.
const TTL = 129600;

async function fetchBisCSV(dataset, key) {
  const separator = key.includes('?') ? '&' : '?';
  const url = `${BIS_BASE}/${dataset}/${key}${separator}format=csv`;
  const resp = await fetch(url, {
    headers: { 'User-Agent': CHROME_UA, Accept: 'text/csv' },
    signal: AbortSignal.timeout(30_000),
  });
  if (!resp.ok) throw new Error(`BIS HTTP ${resp.status} for ${dataset}`);
  return resp.text();
}

function parseBisCSV(csv) {
  const lines = csv.split('\n');
  if (lines.length < 2) return [];
  const headers = parseCSVLine(lines[0]);
  const rows = [];
  for (let i = 1; i < lines.length; i++) {
    const line = lines[i].trim();
    if (!line) continue;
    const vals = parseCSVLine(line);
    const row = {};
    for (let j = 0; j < headers.length; j++) {
      row[headers[j]] = vals[j] || '';
    }
    rows.push(row);
  }
  return rows;
}

function parseCSVLine(line) {
  const result = [];
  let current = '';
  let inQuotes = false;
  for (let i = 0; i < line.length; i++) {
    const ch = line[i];
    if (inQuotes) {
      if (ch === '"' && line[i + 1] === '"') { current += '"'; i++; }
      else if (ch === '"') { inQuotes = false; }
      else { current += ch; }
    } else {
      if (ch === '"') { inQuotes = true; }
      else if (ch === ',') { result.push(current.trim()); current = ''; }
      else { current += ch; }
    }
  }
  result.push(current.trim());
  return result;
}

function parseBisNumber(val) {
  if (!val || val === '.' || val.trim() === '') return null;
  const n = Number(val);
  return Number.isFinite(n) ? n : null;
}

function groupByCountry(rows) {
  const byCountry = new Map();
  for (const row of rows) {
    const cc = row.REF_AREA || row.BORROWERS_CTY || row['Reference area'] || '';
    const date = row.TIME_PERIOD || row['Time period'] || '';
    const val = parseBisNumber(row.OBS_VALUE || row['Observation value']);
    if (!cc || !date || val === null) continue;
    if (!byCountry.has(cc)) byCountry.set(cc, []);
    byCountry.get(cc).push({ date, value: val });
  }
  return byCountry;
}

// --- Policy Rates (WS_CBPOL) ---
async function fetchPolicyRates() {
  const threeMonthsAgo = new Date();
  threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
  const startPeriod = `${threeMonthsAgo.getFullYear()}-${String(threeMonthsAgo.getMonth() + 1).padStart(2, '0')}`;

  const csv = await fetchBisCSV('WS_CBPOL', `M.${BIS_COUNTRY_KEYS}?startPeriod=${startPeriod}&detail=dataonly`);
  const byCountry = groupByCountry(parseBisCSV(csv));

  const rates = [];
  for (const [cc, obs] of byCountry) {
    const info = BIS_COUNTRIES[cc];
    if (!info) continue;
    obs.sort((a, b) => a.date.localeCompare(b.date));
    const latest = obs[obs.length - 1];
    const previous = obs.length >= 2 ? obs[obs.length - 2] : undefined;
    if (latest) {
      rates.push({
        countryCode: cc, countryName: info.name,
        rate: latest.value, previousRate: previous?.value ?? latest.value,
        date: latest.date, centralBank: info.centralBank,
      });
    }
  }
  console.log(`  Policy rates: ${rates.length} countries`);
  return rates.length > 0 ? { rates } : null;
}

// --- Exchange Rates (WS_EER) ---
async function fetchExchangeRates() {
  const threeMonthsAgo = new Date();
  threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
  const startPeriod = `${threeMonthsAgo.getFullYear()}-${String(threeMonthsAgo.getMonth() + 1).padStart(2, '0')}`;

  const csv = await fetchBisCSV('WS_EER', `M.R.B.${BIS_COUNTRY_KEYS}?startPeriod=${startPeriod}&detail=dataonly`);
  const byCountry = groupByCountry(parseBisCSV(csv));

  const rates = [];
  for (const [cc, obs] of byCountry) {
    const info = BIS_COUNTRIES[cc];
    if (!info) continue;
    obs.sort((a, b) => a.date.localeCompare(b.date));
    const latest = obs[obs.length - 1];
    const prev = obs.length >= 2 ? obs[obs.length - 2] : undefined;
    if (latest) {
      const realChange = prev
        ? Math.round(((latest.value - prev.value) / prev.value) * 1000) / 10
        : 0;
      rates.push({
        countryCode: cc, countryName: info.name,
        realEer: Math.round(latest.value * 100) / 100, nominalEer: 0,
        realChange, date: latest.date,
      });
    }
  }
  console.log(`  Exchange rates: ${rates.length} countries`);
  return rates.length > 0 ? { rates } : null;
}

// --- Credit to GDP (WS_TC) ---
async function fetchCreditToGdp() {
  const twoYearsAgo = new Date();
  twoYearsAgo.setFullYear(twoYearsAgo.getFullYear() - 2);
  const startPeriod = `${twoYearsAgo.getFullYear()}-Q1`;

  const csv = await fetchBisCSV('WS_TC', `Q.${BIS_COUNTRY_KEYS}.C.A.M.770.A?startPeriod=${startPeriod}&detail=dataonly`);
  const byCountry = groupByCountry(parseBisCSV(csv));

  const entries = [];
  for (const [cc, obs] of byCountry) {
    const info = BIS_COUNTRIES[cc];
    if (!info) continue;
    obs.sort((a, b) => a.date.localeCompare(b.date));
    const latest = obs[obs.length - 1];
    const previous = obs.length >= 2 ? obs[obs.length - 2] : undefined;
    if (latest) {
      entries.push({
        countryCode: cc, countryName: info.name,
        creditGdpRatio: Math.round(latest.value * 10) / 10,
        previousRatio: previous ? Math.round(previous.value * 10) / 10 : Math.round(latest.value * 10) / 10,
        date: latest.date,
      });
    }
  }
  console.log(`  Credit-to-GDP: ${entries.length} countries`);
  return entries.length > 0 ? { entries } : null;
}

// --- Main seed ---

async function fetchAll() {
  const [policy, exchange, credit] = await Promise.all([
    fetchPolicyRates(),
    fetchExchangeRates(),
    fetchCreditToGdp(),
  ]);
  const total = (policy?.rates?.length || 0) + (exchange?.rates?.length || 0) + (credit?.entries?.length || 0);
  if (total === 0) throw new Error('All BIS fetches returned empty');
  return { policy, exchange, credit };
}

// validateFn receives the post-transform data ({ rates: [...] }), not the raw fetchAll shape.
function validate(data) {
  return Array.isArray(data?.rates) && data.rates.length > 0;
}

// Contract: canonical key stores bis policy rates; declareRecords sees the
// post-transform `{rates: [...]}` shape, same as validateFn.
export function declareRecords(data) {
  return Array.isArray(data?.rates) ? data.rates.length : 0;
}

// publishTransform: store only policy data (correct shape) at canonical key.
// runSeed() calls process.exit(0) — .then() is unreachable; use afterPublish instead.
function publishTransform(data) {
  return data.policy ?? { rates: [] };
}

// Content-age contract: newest BIS policy-rate observation date (the canonical
// key holds `data.policy`). Runs on raw fetchAll() output, before
// publishTransform. See scripts/_content-age-helpers.mjs.
export function bisPolicyContentMeta(data) {
  const rates = Array.isArray(data?.policy?.rates) ? data.policy.rates : [];
  return tokensToContentMeta(rates.map((r) => r?.date));
}

async function afterPublish(data) {
  if (data.exchange) await writeExtraKey(KEYS.exchange, data.exchange, TTL);
  if (data.credit) await writeExtraKey(KEYS.credit, data.credit, TTL);
}

if (process.argv[1]?.endsWith('seed-bis-data.mjs')) {
  runSeed('economic', 'bis', KEYS.policy, fetchAll, {
    validateFn: validate,
    ttlSeconds: TTL,
    sourceVersion: 'bis-sdmx-csv',
    declareRecords,
    schemaVersion: 1,
    maxStaleMin: 10080,
    publishTransform,
    afterPublish,
    contentMeta: bisPolicyContentMeta,
    maxContentAgeMin: BIS_POLICY_MAX_CONTENT_AGE_MIN,
  }).catch((err) => {
    const _cause = err.cause ? ` (cause: ${err.cause.message || err.cause.code || err.cause})` : '';
    console.error('FATAL:', (err.message || err) + _cause);
    process.exit(1);
  });
}
