import type { DB } from "../generated/kysely-tailordb";
import {
  CurrencyNotFoundError,
  ExchangeRateNotFoundError,
  InactiveCurrencyError,
} from "../lib/errors.generated";
import { getCurrency } from "./getCurrency.generated";
import { ok, err, type ReadonlyDB, type QueryContext } from "@tailor-platform/erp-kit/module";

export interface ConvertAmountInput {
  amount: number;
  sourceCurrencyCode: string;
  targetCurrencyCode: string;
  conversionDate: string;
}

/**
 * Converts a monetary amount from one currency to another using the applicable
 * exchange rate for a given date. Currencies are identified by their ISO 4217
 * code (e.g., "USD", "EUR", "JPY"). The function looks up the most recent exchange
 * rate on or before the specified date. If no direct rate exists, it calculates
 * the inverse rate. Result is rounded to the target currency's decimal precision.
 */
export async function run(db: ReadonlyDB<DB>, input: ConvertAmountInput, ctx: QueryContext) {
  // Validate source currency exists
  const { currency: sourceCurrency } = (
    await getCurrency(
      db,
      {
        code: input.sourceCurrencyCode,
      },
      ctx,
    )
  ).value;

  if (!sourceCurrency) {
    return err(new CurrencyNotFoundError(input.sourceCurrencyCode));
  }

  // Validate target currency exists
  const { currency: targetCurrency } = (
    await getCurrency(
      db,
      {
        code: input.targetCurrencyCode,
      },
      ctx,
    )
  ).value;

  if (!targetCurrency) {
    return err(new CurrencyNotFoundError(input.targetCurrencyCode));
  }

  // Validate both currencies are active
  if (!sourceCurrency.isActive) {
    return err(new InactiveCurrencyError(input.sourceCurrencyCode));
  }

  if (!targetCurrency.isActive) {
    return err(new InactiveCurrencyError(input.targetCurrencyCode));
  }

  // Same currency - return original amount
  if (sourceCurrency.id === targetCurrency.id) {
    return ok({
      convertedAmount: input.amount,
      exchangeRate: 1,
      sourceCurrency,
      targetCurrency,
      exchangeRateRecord: null,
    });
  }

  // Find direct exchange rate (most recent on or before conversion date)
  const conversionDate = new Date(input.conversionDate);
  const directRate = await db
    .selectFrom("ExchangeRate")
    .selectAll()
    .where("sourceCurrencyId", "=", sourceCurrency.id)
    .where("targetCurrencyId", "=", targetCurrency.id)
    .where("effectiveDate", "<=", conversionDate)
    .orderBy("effectiveDate", "desc")
    .executeTakeFirst();

  if (directRate) {
    const exchangeRate = directRate.rate;
    const exchangeRateRecord = directRate;

    const rawResult = input.amount * exchangeRate;
    const roundingFactor = Math.pow(10, targetCurrency.decimalPlaces);
    const convertedAmount = Math.round(rawResult * roundingFactor) / roundingFactor;

    return ok({
      convertedAmount,
      exchangeRate,
      sourceCurrency,
      targetCurrency,
      exchangeRateRecord,
    });
  }

  // Try inverse rate
  const inverseRate = await db
    .selectFrom("ExchangeRate")
    .selectAll()
    .where("sourceCurrencyId", "=", targetCurrency.id)
    .where("targetCurrencyId", "=", sourceCurrency.id)
    .where("effectiveDate", "<=", conversionDate)
    .orderBy("effectiveDate", "desc")
    .executeTakeFirst();

  if (!inverseRate) {
    return err(
      new ExchangeRateNotFoundError(
        `${input.sourceCurrencyCode} to ${input.targetCurrencyCode} on ${input.conversionDate}`,
      ),
    );
  }

  const exchangeRate = 1 / inverseRate.rate;
  const exchangeRateRecord = inverseRate;

  const rawResult = input.amount * exchangeRate;
  const roundingFactor = Math.pow(10, targetCurrency.decimalPlaces);
  const convertedAmount = Math.round(rawResult * roundingFactor) / roundingFactor;

  return ok({
    convertedAmount,
    exchangeRate,
    sourceCurrency,
    targetCurrency,
    exchangeRateRecord,
  });
}
