import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import countryToCurrency from "country-to-currency";
import { z } from "zod";

export const ISO_COUNTRY_CODES = `
AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ
BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ
CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ
DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR
GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY
HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP
KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY
MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ
NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY
QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ
TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ
VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW
`.trim().split(/\s+/).sort();

export const PRICING_URL_TEMPLATE = "https://chatgpt.com/backend-anon/checkout_pricing_config/configs/{country_code}";
const FRANKFURTER_URL = "https://api.frankfurter.dev/v1/latest";
const OPEN_EXCHANGE_URL = "https://open.er-api.com/v6/latest/USD";
const DEFAULT_TIMEOUT_MS = 12_000;
const DEFAULT_RETRIES = 3;
const DEFAULT_CONCURRENCY = 4;
const STALE_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;

const upstreamSchema = z.object({
  country_code: z.string().length(2),
  currency_config: z.object({
    business: z.object({
      month: z.object({
        amount: z.number().positive().finite(),
        tax: z.enum(["inclusive", "exclusive"]),
      }),
      year: z.object({
        amount: z.number().positive().finite(),
      }).passthrough().optional(),
    }),
    symbol_code: z.string().length(3).optional(),
    currency_code: z.string().length(3).optional(),
    currency: z.string().length(3).optional(),
    symbol: z.string().min(1).optional(),
  }).passthrough(),
  symbol_code: z.string().length(3).optional(),
  currency_code: z.string().length(3).optional(),
  currency: z.string().length(3).optional(),
  symbol: z.string().min(1).optional(),
  minor_unit_exponent: z.number().int().min(0).max(4).optional(),
  pricing_rollout_gate: z.string().optional(),
  amount_per_credit: z.number().finite().optional(),
  tax_type: z.string().min(1).nullable().optional(),
  tax_percent: z.number().finite().nullable().optional(),
}).passthrough();

const snapshotSchema = z.object({
  version: z.literal(1),
  generatedAt: z.string(),
  rows: z.array(z.object({
    countryCode: z.string().length(2),
    countryName: z.string(),
    currencyCode: z.string().length(3),
    symbol: z.string(),
    minorUnitExponent: z.number().int(),
    localAmount: z.number(),
    usdAmount: z.number(),
    cnyAmount: z.number(),
    taxTreatment: z.enum(["inclusive", "exclusive"]),
    taxType: z.string().nullable(),
    taxPercent: z.number().nullable(),
    status: z.enum(["fresh", "stale"]),
    fetchedAt: z.string(),
    sourceUrl: z.string().url(),
  })),
}).passthrough();

const countryNames = new Intl.DisplayNames(["zh-CN"], { type: "region" });

export function sourceUrl(countryCode) {
  return PRICING_URL_TEMPLATE.replace("{country_code}", countryCode.toUpperCase());
}

export function parsePriceResponse(raw, requestedCode, fetchedAt = new Date().toISOString()) {
  const data = upstreamSchema.parse(raw);
  const countryCode = requestedCode.toUpperCase();
  if (data.country_code.toUpperCase() !== countryCode) {
    throw new Error(`Country mismatch: requested ${countryCode}, received ${data.country_code}`);
  }

  const currency = resolveCurrency(data, countryCode);
  return {
    countryCode,
    countryName: countryNames.of(countryCode) || countryCode,
    currencyCode: currency.code,
    currencySource: currency.source,
    symbol: data.symbol || data.currency_config.symbol || currencySymbol(currency.code),
    minorUnitExponent: data.minor_unit_exponent ?? currencyDigits(currency.code),
    localAmount: data.currency_config.business.month.amount,
    taxTreatment: data.currency_config.business.month.tax,
    taxType: data.tax_type ?? null,
    taxPercent: data.tax_percent ?? null,
    status: "fresh",
    fetchedAt,
    sourceUrl: sourceUrl(countryCode),
  };
}

export function convertAmount(localAmount, currencyCode, rates) {
  const localRate = currencyCode === "USD" ? 1 : Number(rates[currencyCode]);
  const cnyRate = Number(rates.CNY);
  if (!Number.isFinite(localRate) || localRate <= 0) throw new Error(`Missing FX rate for ${currencyCode}`);
  if (!Number.isFinite(cnyRate) || cnyRate <= 0) throw new Error("Missing FX rate for CNY");
  const rawUsd = localAmount / localRate;
  return {
    usdAmount: roundMoney(rawUsd),
    cnyAmount: roundMoney(rawUsd * cnyRate),
  };
}

export function withConvertedAmounts(row, rates) {
  return { ...row, ...convertAmount(row.localAmount, row.currencyCode, rates) };
}

export function mergeStaleRows(freshRows, failedCodes, previousRows, rates, now = new Date()) {
  const freshCodes = new Set(freshRows.map((row) => row.countryCode));
  const failed = new Set(failedCodes);
  const staleRows = previousRows
    .filter((row) => failed.has(row.countryCode) && !freshCodes.has(row.countryCode))
    .filter((row) => now.getTime() - new Date(row.fetchedAt).getTime() <= STALE_MAX_AGE_MS)
    .map((row) => withConvertedAmounts({ ...row, status: "stale" }, rates));
  return [...freshRows, ...staleRows];
}

export function validateCoverage(snapshot, previousSnapshot = null, minimumFirstRunRows = 20) {
  const freshUs = snapshot.rows.find((row) => row.countryCode === "US" && row.status === "fresh");
  if (!freshUs) throw new Error("Coverage guard failed: a fresh US Business price is required.");
  const freshNonUsd = snapshot.rows.find((row) => row.currencyCode !== "USD" && row.status === "fresh");
  if (!freshNonUsd) throw new Error("Coverage guard failed: a fresh non-USD Business price is required.");
  if (!Number.isFinite(Number(snapshot.fx.rates.CNY))) throw new Error("Coverage guard failed: CNY FX rate is required.");

  if (previousSnapshot?.rows?.length) {
    const minimum = Math.ceil(previousSnapshot.rows.length * 0.9);
    if (snapshot.rows.length < minimum) {
      throw new Error(`Coverage guard failed: ${snapshot.rows.length} rows is below 90% of previous ${previousSnapshot.rows.length}.`);
    }
  } else if (snapshot.rows.length < minimumFirstRunRows) {
    throw new Error(`Coverage guard failed: first live snapshot requires at least ${minimumFirstRunRows} rows.`);
  }
}

async function fetchWithTimeout(url, options = {}) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
  try {
    return await fetch(url, {
      headers: {
        ...requestHeaders(url),
        ...options.headers,
      },
      signal: controller.signal,
    });
  } finally {
    clearTimeout(timer);
  }
}

export async function fetchJsonWithRetry(url, options = {}) {
  const retries = options.retries ?? DEFAULT_RETRIES;
  let lastError;
  for (let attempt = 0; attempt < retries; attempt += 1) {
    try {
      const response = await fetchWithTimeout(url, options);
      if (response.status === 404) return { kind: "unsupported", status: 404 };
      if (!response.ok) {
        const detail = await responseDetail(response);
        const error = new Error(`HTTP ${response.status} for ${url}${detail}`);
        if (response.status < 500 && response.status !== 429) throw Object.assign(error, { nonRetryable: true });
        throw error;
      }
      const contentType = response.headers?.get?.("content-type") || "unknown";
      const body = await response.text();
      try {
        return { kind: "success", data: JSON.parse(body), status: response.status };
      } catch {
        const preview = safePreview(body);
        throw Object.assign(
          new Error(`Invalid JSON from ${url}; content-type=${contentType}; body=${preview}`),
          { nonRetryable: true },
        );
      }
    } catch (error) {
      lastError = error;
      if (error?.nonRetryable || attempt === retries - 1) break;
      await sleep((attempt + 1) * 850 + Math.floor(Math.random() * 250));
    }
  }
  throw lastError;
}

async function collectCountry(countryCode, fetchedAt) {
  const result = await fetchJsonWithRetry(sourceUrl(countryCode));
  if (result.kind === "unsupported") return { kind: "unsupported", countryCode };
  return {
    kind: "success",
    countryCode,
    responseKeys: Object.keys(result.data || {}).sort(),
    row: parsePriceResponse(result.data, countryCode, fetchedAt),
  };
}

function resolveCurrency(data, countryCode) {
  const explicit = data.symbol_code
    || data.currency_code
    || data.currency
    || data.currency_config.symbol_code
    || data.currency_config.currency_code
    || data.currency_config.currency;
  if (explicit) return { code: explicit.toUpperCase(), source: "api" };

  const rolloutCurrency = data.pricing_rollout_gate?.match(/pricing_enabled_for_([a-z]{3})/i)?.[1];
  if (rolloutCurrency) return { code: rolloutCurrency.toUpperCase(), source: "rollout" };

  const monthAmount = data.currency_config.business.month.amount;
  const yearAmount = data.currency_config.business.year?.amount;
  const oneDollarAmount = data.promos?.business_one_dollar?.amount;
  const looksLikeUsdProfile = monthAmount === 25
    && (yearAmount === 20 || data.amount_per_credit === 0.04 || oneDollarAmount === 1);
  if (looksLikeUsdProfile) return { code: "USD", source: "usd-profile" };

  const countryCurrency = countryToCurrency[countryCode];
  if (countryCurrency) return { code: countryCurrency, source: "country-default" };
  throw new Error(`Currency metadata is missing for ${countryCode} and no safe fallback is available.`);
}

function currencySymbol(currencyCode) {
  try {
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: currencyCode,
      currencyDisplay: "narrowSymbol",
    }).formatToParts(0).find((part) => part.type === "currency")?.value || currencyCode;
  } catch {
    return currencyCode;
  }
}

function currencyDigits(currencyCode) {
  try {
    return new Intl.NumberFormat("en-US", { style: "currency", currency: currencyCode })
      .resolvedOptions().maximumFractionDigits;
  } catch {
    return 2;
  }
}

function requestHeaders(url) {
  const parsed = new URL(url);
  const common = {
    accept: "application/json, text/plain, */*",
    "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
    "cache-control": "no-cache",
    pragma: "no-cache",
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
  };
  if (parsed.hostname !== "chatgpt.com" || !parsed.pathname.startsWith("/backend-anon/checkout_pricing_config/configs/")) {
    return common;
  }
  return {
    ...common,
    origin: "https://chatgpt.com",
    referer: "https://chatgpt.com/zh-Hans-CN/pricing/",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-origin",
    "x-openai-target-path": parsed.pathname,
    "x-openai-target-route": "/backend-anon/checkout_pricing_config/configs/{country_code}",
  };
}

async function responseDetail(response) {
  const contentType = response.headers?.get?.("content-type") || "unknown";
  const server = response.headers?.get?.("server") || "unknown";
  const ray = response.headers?.get?.("cf-ray") || "none";
  let body = "";
  try {
    body = await response.text();
  } catch {
    body = "<unreadable>";
  }
  return `; content-type=${contentType}; server=${server}; cf-ray=${ray}; body=${safePreview(body)}`;
}

function safePreview(value) {
  return JSON.stringify(String(value || "").replace(/\s+/g, " ").slice(0, 240));
}

async function collectAllCountries(countryCodes, fetchedAt, concurrency = DEFAULT_CONCURRENCY) {
  const results = await mapLimit(countryCodes, concurrency, async (countryCode) => {
    try {
      return await collectCountry(countryCode, fetchedAt);
    } catch (error) {
      return {
        kind: "failed",
        countryCode,
        error: error instanceof Error ? error.message : String(error),
      };
    }
  });

  return {
    rows: results.filter((item) => item.kind === "success").map((item) => item.row),
    unsupportedCodes: results.filter((item) => item.kind === "unsupported").map((item) => item.countryCode),
    failedItems: results.filter((item) => item.kind === "failed"),
  };
}

export async function fetchFxSnapshot(currencies) {
  const wanted = [...new Set(["CNY", ...currencies.filter((item) => item !== "USD")])].sort();
  const rates = { USD: 1 };
  const sourceUrls = [];
  let primaryDate = null;
  let primaryWorked = false;

  const primaryUrl = `${FRANKFURTER_URL}?base=USD&symbols=${encodeURIComponent(wanted.join(","))}`;
  sourceUrls.push(primaryUrl);
  try {
    const result = await fetchJsonWithRetry(primaryUrl, { retries: 2 });
    if (result.kind === "success") {
      Object.assign(rates, numericRates(result.data.rates));
      primaryDate = result.data.date || null;
      primaryWorked = true;
    }
  } catch (error) {
    console.warn(`Frankfurter unavailable: ${error instanceof Error ? error.message : error}`);
  }

  const missing = wanted.filter((currency) => !Number.isFinite(Number(rates[currency])));
  let fallbackDate = null;
  if (missing.length) {
    sourceUrls.push(OPEN_EXCHANGE_URL);
    const fallback = await fetchJsonWithRetry(OPEN_EXCHANGE_URL);
    if (fallback.kind !== "success") throw new Error("FX fallback unexpectedly returned no data.");
    const fallbackRates = numericRates(fallback.data.rates);
    for (const currency of missing) {
      if (Number.isFinite(fallbackRates[currency])) rates[currency] = fallbackRates[currency];
    }
    fallbackDate = dateFromOpenExchange(fallback.data.time_last_update_utc);
  }

  const stillMissing = wanted.filter((currency) => !Number.isFinite(Number(rates[currency])));
  if (stillMissing.length) throw new Error(`Missing FX rates for ${stillMissing.join(", ")}`);

  return {
    baseCurrency: "USD",
    date: primaryDate || fallbackDate || new Date().toISOString().slice(0, 10),
    source: missing.length ? (primaryWorked ? "Frankfurter + open.er-api.com" : "open.er-api.com") : "Frankfurter",
    sourceUrls,
    rates,
  };
}

function numericRates(value) {
  if (!value || typeof value !== "object") return {};
  return Object.fromEntries(
    Object.entries(value)
      .map(([currency, rate]) => [currency, Number(rate)])
      .filter(([, rate]) => Number.isFinite(rate) && rate > 0),
  );
}

function dateFromOpenExchange(value) {
  const date = new Date(value || "");
  return Number.isNaN(date.getTime()) ? null : date.toISOString().slice(0, 10);
}

async function readPreviousSnapshot(urlOrPath) {
  if (!urlOrPath) return null;
  try {
    let raw;
    if (/^https?:\/\//i.test(urlOrPath)) {
      const result = await fetchJsonWithRetry(urlOrPath, { retries: 1, timeoutMs: 8_000 });
      if (result.kind !== "success") return null;
      raw = result.data;
    } else {
      raw = JSON.parse(await readFile(urlOrPath, "utf8"));
    }
    return snapshotSchema.parse(raw);
  } catch (error) {
    console.warn(`Previous snapshot unavailable: ${error instanceof Error ? error.message : error}`);
    return null;
  }
}

export async function collectSnapshot(options = {}) {
  const generatedAt = options.generatedAt || new Date().toISOString();
  const countryCodes = options.countryCodes || ISO_COUNTRY_CODES;
  const previousSnapshot = options.previousSnapshot || null;
  const collected = await collectAllCountries(countryCodes, generatedAt, options.concurrency);
  logCollectionDiagnostics(collected, countryCodes.length);
  const currencies = collected.rows.map((row) => row.currencyCode);
  for (const failed of collected.failedItems) {
    const previous = previousSnapshot?.rows?.find((row) => row.countryCode === failed.countryCode);
    if (previous) currencies.push(previous.currencyCode);
  }
  const fx = await fetchFxSnapshot(currencies);
  const freshRows = collected.rows.map((row) => withConvertedAmounts(row, fx.rates));
  const rows = mergeStaleRows(
    freshRows,
    collected.failedItems.map((item) => item.countryCode),
    previousSnapshot?.rows || [],
    fx.rates,
    new Date(generatedAt),
  ).sort((a, b) => a.cnyAmount - b.cnyAmount);

  return {
    version: 1,
    mode: "live",
    generatedAt,
    pricingSource: {
      name: "OpenAI ChatGPT checkout pricing config",
      urlTemplate: PRICING_URL_TEMPLATE,
    },
    fx,
    coverage: {
      requested: countryCodes.length,
      success: freshRows.length,
      unsupported: collected.unsupportedCodes.length,
      failed: collected.failedItems.length,
      stale: rows.filter((row) => row.status === "stale").length,
    },
    rows,
  };
}

function logCollectionDiagnostics(collected, requested) {
  console.log(
    `[pricing] requested=${requested} success=${collected.rows.length} unsupported=${collected.unsupportedCodes.length} failed=${collected.failedItems.length}`,
  );
  if (!collected.failedItems.length) return;
  const prioritized = [...collected.failedItems].sort((a, b) => {
    if (a.countryCode === "US") return -1;
    if (b.countryCode === "US") return 1;
    return a.countryCode.localeCompare(b.countryCode);
  });
  for (const item of prioritized.slice(0, 12)) {
    console.warn(`[pricing] ${item.countryCode} failed: ${item.error}`);
  }
  if (prioritized.length > 12) console.warn(`[pricing] ${prioritized.length - 12} additional failures omitted.`);
}

async function runCli() {
  const probeCode = cliValue("--probe");
  if (probeCode) {
    const result = await collectCountry(probeCode.toUpperCase(), new Date().toISOString());
    if (result.kind !== "success") throw new Error(`Pricing probe ${probeCode.toUpperCase()} returned ${result.kind}.`);
    console.log(
      `[pricing] probe ${result.countryCode} ok: ${result.row.currencyCode} ${result.row.localAmount}; currency-source=${result.row.currencySource}; response-keys=${result.responseKeys.join(",")}`,
    );
    return;
  }
  const outputPath = cliValue("--output") || process.env.OUTPUT_PATH || "public/data/prices.json";
  const previousSource = process.env.PREVIOUS_SNAPSHOT_URL || process.env.PREVIOUS_SNAPSHOT_PATH || null;
  const previousSnapshot = await readPreviousSnapshot(previousSource);
  const snapshot = await collectSnapshot({
    previousSnapshot,
    concurrency: Number(process.env.PRICING_CONCURRENCY || DEFAULT_CONCURRENCY),
  });
  validateCoverage(snapshot, previousSnapshot, Number(process.env.MIN_FIRST_RUN_ROWS || 20));
  await mkdir(path.dirname(outputPath), { recursive: true });
  await writeFile(outputPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
  console.log(JSON.stringify({ outputPath, generatedAt: snapshot.generatedAt, coverage: snapshot.coverage, fx: snapshot.fx.source }, null, 2));
}

function cliValue(name) {
  const index = process.argv.indexOf(name);
  return index >= 0 ? process.argv[index + 1] : null;
}

async function mapLimit(items, limit, mapper) {
  const output = new Array(items.length);
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const index = next;
      next += 1;
      output[index] = await mapper(items[index], index);
    }
  }
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
  return output;
}

function roundMoney(value) {
  return Math.round((value + Number.EPSILON) * 100) / 100;
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

const isMain = process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
if (isMain) {
  runCli().catch((error) => {
    console.error(error instanceof Error ? error.stack || error.message : error);
    process.exitCode = 1;
  });
}
