import { eq } from 'drizzle-orm';

import { db } from '@/lib/db';
import { ports } from '@/lib/db/schema/ports';
import { getRate } from '@/lib/services/currency';
import { logger } from '@/lib/logger';

/**
 * Port's default currency for money normalisation. Falls back to USD
 * when the column is null (legacy ports). Shared across the report
 * services so every money figure lands in one reporting currency.
 */
export async function resolvePortCurrency(portId: string): Promise<string> {
  const [row] = await db
    .select({ defaultCurrency: ports.defaultCurrency })
    .from(ports)
    .where(eq(ports.id, portId));
  return row?.defaultCurrency ?? 'USD';
}

/**
 * Convert `amount` from `from` → `to`, rounding the result to 2dp.
 *
 * Used for one-off per-row figures in report TABLES (recent payments,
 * expense ledger, …) where a single normalised display value is wanted and
 * the cent-rounding is correct because the value is shown standalone, not
 * accumulated. For SUMS use {@link CurrencyAccumulator} instead — accumulating
 * per-row `normalizeAmount` results compounds rounding drift (audit M19).
 *
 * On a missing/stale rate this returns `null` rather than the former silent
 * `?? amount` fallback that added an unconverted foreign amount straight into
 * a port-currency figure (audit L25). Callers decide how to degrade.
 */
export async function normalizeAmount(
  amount: number,
  from: string,
  to: string,
): Promise<number | null> {
  if (!amount) return amount;
  const f = from.toUpperCase();
  const t = to.toUpperCase();
  if (f === t) return amount;
  const rate = await getRate(f, t);
  if (rate == null) {
    logger.warn({ from: f, to: t }, 'Report normalizeAmount: FX rate unavailable; skipping figure');
    return null;
  }
  return Number((amount * rate).toFixed(2));
}

/**
 * Sums money amounts in their SOURCE currency, grouped by currency, then
 * converts each currency bucket to the target currency exactly ONCE at
 * settle time — rounding only the final per-target figure (audit M19).
 *
 * This avoids two correctness bugs of the old per-row `normalizeAmount`-then-
 * accumulate pattern:
 *   1. cents-rounding every row before adding compounded ±0.5¢×N drift;
 *   2. a missing/stale rate silently added the raw foreign amount into the
 *      port-currency total (audit L25) — here an unconvertible bucket is
 *      skipped and counted, never folded in at the wrong scale.
 */
export class CurrencyAccumulator {
  /** sourceCurrency (upper) → summed raw amount in that source currency */
  private readonly buckets = new Map<string, number>();

  /** Add a raw amount in its own currency. No conversion happens here. */
  add(amount: number, currency: string): void {
    if (!amount) return;
    const c = currency.toUpperCase();
    this.buckets.set(c, (this.buckets.get(c) ?? 0) + amount);
  }

  /**
   * Convert every bucket to `target` once, sum, and round only the final
   * figure. Unconvertible buckets (no/stale rate) are skipped and counted in
   * `unconvertible` rather than added at the wrong scale.
   */
  async settle(target: string): Promise<{ total: number; unconvertible: number }> {
    const t = target.toUpperCase();
    let total = 0;
    let unconvertible = 0;
    for (const [currency, sum] of this.buckets) {
      if (sum === 0) continue;
      if (currency === t) {
        total += sum;
        continue;
      }
      const rate = await getRate(currency, t);
      if (rate == null) {
        unconvertible += 1;
        logger.warn(
          { from: currency, to: t, amount: sum },
          'Report aggregate: FX rate unavailable; bucket excluded from total',
        );
        continue;
      }
      total += sum * rate;
    }
    return { total: Number(total.toFixed(2)), unconvertible };
  }
}
