import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { Currency } from '@prisma/client';

/**
 * Minor-unit decimal places per currency, used to round the CHARGE amount sent to the payment
 * microservice. DJF has no minor unit (whole francs only); ETB and USD use 2 decimals.
 */
const CHARGE_CURRENCY_DECIMALS: Record<string, number> = {
  ETB: 2,
  USD: 2,
  DJF: 0,
};

@Injectable()
export class CurrencyService {
  private readonly logger = new Logger(CurrencyService.name);

  constructor(private readonly prisma: PrismaService) {}

  /**
   * Converts a stored display-currency minor amount to the charge major amount
   * sent to the payment provider, without hitting the DB for an exchange rate.
   * Use this when the payment method's settlement currency matches the booking's
   * displayCurrency — the rate is already baked into displayTotalMinor.
   */
  displayMinorToChargeMajor(displayMinor: number, currency: string): number {
    const decimals = CHARGE_CURRENCY_DECIMALS[currency.toUpperCase()];
    if (decimals === undefined) {
      throw new BadRequestException(`Unsupported charge currency: ${currency}`);
    }
    return this.roundTo(displayMinor / 100, decimals);
  }

  async convertEtbMinorToChargeMinor(
    amountMinorEtb: number,
    targetCurrency: string,
  ): Promise<number> {
    const target = targetCurrency.toUpperCase();
    if (CHARGE_CURRENCY_DECIMALS[target] === undefined) {
      throw new BadRequestException(`Unsupported charge currency: ${targetCurrency}`);
    }

    if (target === Currency.ETB) {
      return amountMinorEtb;
    }

    // Convert ETB minor → target minor: apply exchange rate, keep as minor units.
    const rate = await this.getRateOrThrow(Currency.ETB, target as Currency);
    return Math.round(amountMinorEtb * rate);
  }

  /**
   * Converts a booking's stored minor-unit amount (in its own `fromCurrency`) to the charge
   * major-unit amount sent to the payment provider in `targetCurrency`. When the two currencies
   * match, no exchange rate is applied — the stored amount is charged as-is. Otherwise the
   * fromCurrency→targetCurrency rate is applied. In both cases the result is divided by 100 to
   * yield major units and rounded to the target currency's precision
   * (e.g. 300000 ETB minor → 3000.00 ETB major; DJF rounds to whole francs).
   */
  async convertMinorToChargeMajor(
    amountMinor: number,
    fromCurrency: string,
    targetCurrency: string,
  ): Promise<number> {
    const from = fromCurrency.toUpperCase();
    const target = targetCurrency.toUpperCase();
    if (CHARGE_CURRENCY_DECIMALS[target] === undefined) {
      throw new BadRequestException(`Unsupported charge currency: ${targetCurrency}`);
    }
    const decimals = CHARGE_CURRENCY_DECIMALS[target];

    if (from === target) {
      return this.roundTo(amountMinor / 100, decimals);
    }

    const rate = await this.getRateOrThrow(from as Currency, target as Currency);
    return this.roundTo((amountMinor * rate) / 100, decimals);
  }

  async getRateOrThrow(
    fromCurrency: Currency,
    toCurrency: Currency,
  ): Promise<number> {
    if (fromCurrency === toCurrency) return 1;

    // Direct rate
    const direct = await this.prisma.currencyExchangeRate.findFirst({
      where: { fromCurrency, toCurrency },
      orderBy: { effectiveDate: 'desc' },
    });
    if (direct) return Number(direct.rate);

    // Inverse rate
    const inverse = await this.prisma.currencyExchangeRate.findFirst({
      where: { fromCurrency: toCurrency, toCurrency: fromCurrency },
      orderBy: { effectiveDate: 'desc' },
    });
    if (inverse) return 1 / Number(inverse.rate);

    // Bridge via ETB (e.g. DJF→USD = (DJF→ETB) × (ETB→USD))
    if (fromCurrency !== Currency.ETB && toCurrency !== Currency.ETB) {
      const toEtb = await this.getRateOrThrow(fromCurrency, Currency.ETB);
      const etbToTarget = await this.getRateOrThrow(Currency.ETB, toCurrency);
      return toEtb * etbToTarget;
    }

    throw new BadRequestException(
      `No exchange rate configured for ${fromCurrency}->${toCurrency}`,
    );
  }

  private roundTo(value: number, decimals: number): number {
    const factor = 10 ** decimals;
    return Math.round(value * factor) / factor;
  }

  async convertAmount(
    amountMinor: number,
    fromCurrency: Currency,
    toCurrency: Currency,
  ): Promise<number> {
    if (fromCurrency === toCurrency) {
      return amountMinor;
    }

    const rate = await this.getExchangeRate(fromCurrency, toCurrency);
    return amountMinor * rate;
  }

  async getExchangeRate(
    fromCurrency: Currency,
    toCurrency: Currency,
  ): Promise<number> {
    if (fromCurrency === toCurrency) return 1;

    const exchangeRate = await this.prisma.currencyExchangeRate.findFirst({
      where: { fromCurrency, toCurrency },
      orderBy: { effectiveDate: 'desc' },
    });

    if (!exchangeRate) {
      // H-2: fail closed. Never price at parity (1.0) when a required rate is absent — a silent 1.0
      // substitution underprices international fares ~100×. Reject the quote/booking instead.
      this.logger.error(
        `No exchange rate configured for ${fromCurrency}->${toCurrency}; refusing to price at parity`,
      );
      throw new BadRequestException(
        `No exchange rate configured for ${fromCurrency}->${toCurrency}`,
      );
    }

    const ageMs = Date.now() - exchangeRate.effectiveDate.getTime();
    if (ageMs > 2 * 24 * 60 * 60 * 1000) {
      this.logger.warn(
        `Stale exchange rate for ${fromCurrency}->${toCurrency}: last updated ${exchangeRate.effectiveDate.toISOString()}`,
      );
    }

    return Number(exchangeRate.rate);
  }

  async listRates() {
    return this.prisma.currencyExchangeRate.findMany({
      orderBy: [{ fromCurrency: 'asc' }, { toCurrency: 'asc' }, { effectiveDate: 'desc' }],
    });
  }

  async upsertRate(
    fromCurrency: Currency,
    toCurrency: Currency,
    rate: number,
    effectiveDate?: Date,
    source = 'MANUAL',
  ) {
    const date = effectiveDate ?? this.todayUtc();
    return this.prisma.currencyExchangeRate.upsert({
      where: { fromCurrency_toCurrency_effectiveDate: { fromCurrency, toCurrency, effectiveDate: date } },
      update: { rate, source },
      create: { fromCurrency, toCurrency, rate, effectiveDate: date, source },
    });
  }

  async updateRateById(id: string, rate: number, source = 'MANUAL') {
    const existing = await this.prisma.currencyExchangeRate.findUnique({ where: { id } });
    if (!existing) throw new NotFoundException('Exchange rate not found');
    return this.prisma.currencyExchangeRate.update({ where: { id }, data: { rate, source } });
  }

  async deleteRate(id: string) {
    return this.prisma.currencyExchangeRate.delete({ where: { id } });
  }

  /** Returns midnight UTC for today — used as the date-only key for upserts. */
  private todayUtc(): Date {
    const d = new Date();
    d.setUTCHours(0, 0, 0, 0);
    return d;
  }
}
