import { db } from '@/lib/db';
import { currencyRates } from '@/lib/db/schema/system';
import { eq, and } from 'drizzle-orm';
import { CodedError } from '@/lib/errors';
import { logger } from '@/lib/logger';
import { fetchWithTimeout } from '@/lib/fetch-with-timeout';

/**
 * Reject FX rates older than this when normalising money for reports (audit
 * L25). A rate this stale almost certainly means the refresh job has been
 * failing silently; using it would mis-state revenue against a months-old
 * cross-rate. 14 days gives ample slack over the daily refresh cadence while
 * still catching a genuinely-stalled poller.
 */
export const RATE_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;

export async function getRate(from: string, to: string): Promise<number | null> {
  if (from === to) return 1;
  const rate = await db.query.currencyRates.findFirst({
    where: and(eq(currencyRates.baseCurrency, from), eq(currencyRates.targetCurrency, to)),
  });
  if (!rate) return null;
  // Staleness guard (L25): a rate the refresh job hasn't touched in
  // RATE_MAX_AGE_MS is treated as unavailable rather than silently used.
  if (rate.fetchedAt && Date.now() - new Date(rate.fetchedAt).getTime() > RATE_MAX_AGE_MS) {
    logger.warn(
      { from, to, fetchedAt: rate.fetchedAt },
      'Currency rate is stale beyond RATE_MAX_AGE_MS; treating as unavailable',
    );
    return null;
  }
  return Number(rate.rate);
}

export async function convert(
  amount: number,
  from: string,
  to: string,
): Promise<{ result: number; rate: number } | null> {
  const rate = await getRate(from, to);
  if (!rate) return null;
  return { result: Number((amount * rate).toFixed(2)), rate };
}

export async function refreshRates(): Promise<void> {
  try {
    const res = await fetchWithTimeout('https://api.frankfurter.dev/v1/latest?base=USD');
    if (!res.ok)
      throw new CodedError('INTERNAL', {
        internalMessage: `Frankfurter API error: ${res.status}`,
      });
    const data = (await res.json()) as { rates: Record<string, number> };
    const rates = data.rates;

    for (const [currency, rate] of Object.entries(rates)) {
      await db
        .insert(currencyRates)
        .values({
          baseCurrency: 'USD',
          targetCurrency: currency,
          rate: String(rate),
          source: 'frankfurter',
          fetchedAt: new Date(),
        })
        .onConflictDoUpdate({
          target: [currencyRates.baseCurrency, currencyRates.targetCurrency],
          set: { rate: String(rate), fetchedAt: new Date(), source: 'frankfurter' },
        });
    }

    // Store inverse rates for common conversions. Persist at full precision —
    // pre-rounding the inverse to 6dp (former behaviour) meant X→USD and
    // USD→X were not exact reciprocals, compounding into report totals (M19).
    // The `rate` column is unbounded `numeric`, so full precision is safe.
    for (const [currency, rate] of Object.entries(rates)) {
      const inverse = String(1 / rate);
      await db
        .insert(currencyRates)
        .values({
          baseCurrency: currency,
          targetCurrency: 'USD',
          rate: inverse,
          source: 'frankfurter',
          fetchedAt: new Date(),
        })
        .onConflictDoUpdate({
          target: [currencyRates.baseCurrency, currencyRates.targetCurrency],
          set: { rate: inverse, fetchedAt: new Date(), source: 'frankfurter' },
        });
    }

    logger.info({ rateCount: Object.keys(rates).length }, 'Currency rates refreshed');
  } catch (err) {
    // Log AND rethrow (L25): swallowing here let a months-stale rate persist
    // unnoticed. The HTTP route wraps this in errorResponse(); the BullMQ
    // maintenance worker marks the job failed and surfaces it via its
    // `failed` handler — either way the failure is now visible.
    logger.error({ err }, 'Failed to refresh currency rates');
    throw err;
  }
}
