/**
 * CurrencyService - Currency and Exchange Rate Management
 *
 * Provides centralized currency operations:
 * - Currency CRUD operations
 * - Exchange rate management
 * - Currency conversion calculations
 * - Rate history tracking
 *
 * @module Core
 */

import { Repository, LessThanOrEqual } from 'typeorm';
import { Currency } from '../entities/currency.entity';
import { CurrencyRate, RateSource } from '../entities/currency-rate.entity';

/**
 * Service context for multi-tenant operations
 */
export interface ServiceContext {
  tenantId: string;
  userId?: string;
}

/**
 * Paginated result interface
 */
export interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

export interface CreateCurrencyRateDto {
  fromCurrencyId: string;
  toCurrencyId: string;
  rate: number;
  rateDate: Date;
  source?: RateSource;
}

export interface CurrencyRateFilters {
  fromCurrencyId?: string;
  toCurrencyId?: string;
  startDate?: Date;
  endDate?: Date;
  source?: RateSource;
}

export interface ConversionResult {
  fromCurrency: string;
  toCurrency: string;
  originalAmount: number;
  convertedAmount: number;
  rate: number;
  rateDate: Date;
}

export class CurrencyService {
  private currencyRepository: Repository<Currency>;
  private rateRepository: Repository<CurrencyRate>;

  constructor(
    currencyRepository: Repository<Currency>,
    rateRepository: Repository<CurrencyRate>
  ) {
    this.currencyRepository = currencyRepository;
    this.rateRepository = rateRepository;
  }

  // ==================== CURRENCY OPERATIONS ====================

  /**
   * Get all active currencies
   */
  async findAllCurrencies(): Promise<Currency[]> {
    return this.currencyRepository.find({
      where: { active: true },
      order: { code: 'ASC' },
    });
  }

  /**
   * Find currency by ID
   */
  async findCurrencyById(id: string): Promise<Currency | null> {
    return this.currencyRepository.findOne({
      where: { id },
    });
  }

  /**
   * Find currency by code (e.g., 'MXN', 'USD')
   */
  async findCurrencyByCode(code: string): Promise<Currency | null> {
    return this.currencyRepository.findOne({
      where: { code: code.toUpperCase() },
    });
  }

  /**
   * Get default currency (typically the first one or configured one)
   */
  async getDefaultCurrency(): Promise<Currency | null> {
    // By convention, MXN is the default for Mexican ERP
    const mxn = await this.findCurrencyByCode('MXN');
    if (mxn) return mxn;

    // Fallback to first active currency
    const currencies = await this.findAllCurrencies();
    return currencies.length > 0 ? currencies[0] : null;
  }

  // ==================== EXCHANGE RATE OPERATIONS ====================

  /**
   * Create a new exchange rate
   */
  async createRate(
    ctx: ServiceContext,
    data: CreateCurrencyRateDto
  ): Promise<CurrencyRate> {
    // Validate currencies exist
    const fromCurrency = await this.findCurrencyById(data.fromCurrencyId);
    const toCurrency = await this.findCurrencyById(data.toCurrencyId);

    if (!fromCurrency) {
      throw new Error('From currency not found');
    }
    if (!toCurrency) {
      throw new Error('To currency not found');
    }
    if (data.fromCurrencyId === data.toCurrencyId) {
      throw new Error('From and To currencies must be different');
    }
    if (data.rate <= 0) {
      throw new Error('Exchange rate must be positive');
    }

    const entity = this.rateRepository.create({
      tenantId: ctx.tenantId,
      fromCurrencyId: data.fromCurrencyId,
      toCurrencyId: data.toCurrencyId,
      rate: data.rate,
      rateDate: data.rateDate,
      source: data.source ?? 'manual',
      createdBy: ctx.userId ?? null,
    });

    return this.rateRepository.save(entity);
  }

  /**
   * Get exchange rate for a specific date
   * Falls back to most recent rate if exact date not found
   */
  async getRate(
    ctx: ServiceContext,
    fromCurrencyId: string,
    toCurrencyId: string,
    date?: Date
  ): Promise<CurrencyRate | null> {
    const rateDate = date || new Date();

    // First try exact date match
    let rate = await this.rateRepository.findOne({
      where: {
        tenantId: ctx.tenantId,
        fromCurrencyId,
        toCurrencyId,
        rateDate,
      },
      relations: ['fromCurrency', 'toCurrency'],
    });

    if (rate) return rate;

    // Fall back to most recent rate before the date
    rate = await this.rateRepository.findOne({
      where: {
        tenantId: ctx.tenantId,
        fromCurrencyId,
        toCurrencyId,
        rateDate: LessThanOrEqual(rateDate),
      },
      order: { rateDate: 'DESC' },
      relations: ['fromCurrency', 'toCurrency'],
    });

    return rate;
  }

  /**
   * Get exchange rate by currency codes
   */
  async getRateByCode(
    ctx: ServiceContext,
    fromCode: string,
    toCode: string,
    date?: Date
  ): Promise<CurrencyRate | null> {
    const fromCurrency = await this.findCurrencyByCode(fromCode);
    const toCurrency = await this.findCurrencyByCode(toCode);

    if (!fromCurrency || !toCurrency) {
      return null;
    }

    return this.getRate(ctx, fromCurrency.id, toCurrency.id, date);
  }

  /**
   * Get rate history for a currency pair
   */
  async getRateHistory(
    ctx: ServiceContext,
    filters: CurrencyRateFilters,
    page = 1,
    limit = 50
  ): Promise<PaginatedResult<CurrencyRate>> {
    const qb = this.rateRepository
      .createQueryBuilder('rate')
      .leftJoinAndSelect('rate.fromCurrency', 'fromCurrency')
      .leftJoinAndSelect('rate.toCurrency', 'toCurrency')
      .where('rate.tenant_id = :tenantId', { tenantId: ctx.tenantId });

    if (filters.fromCurrencyId) {
      qb.andWhere('rate.from_currency_id = :fromCurrencyId', {
        fromCurrencyId: filters.fromCurrencyId,
      });
    }
    if (filters.toCurrencyId) {
      qb.andWhere('rate.to_currency_id = :toCurrencyId', {
        toCurrencyId: filters.toCurrencyId,
      });
    }
    if (filters.startDate) {
      qb.andWhere('rate.rate_date >= :startDate', { startDate: filters.startDate });
    }
    if (filters.endDate) {
      qb.andWhere('rate.rate_date <= :endDate', { endDate: filters.endDate });
    }
    if (filters.source) {
      qb.andWhere('rate.source = :source', { source: filters.source });
    }

    const skip = (page - 1) * limit;
    qb.orderBy('rate.rate_date', 'DESC').skip(skip).take(limit);

    const [data, total] = await qb.getManyAndCount();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  /**
   * Get latest rates for all currency pairs
   */
  async getLatestRates(ctx: ServiceContext): Promise<CurrencyRate[]> {
    // Get distinct currency pairs with their latest rate
    const subQuery = this.rateRepository
      .createQueryBuilder('sub')
      .select('sub.from_currency_id')
      .addSelect('sub.to_currency_id')
      .addSelect('MAX(sub.rate_date)', 'max_date')
      .where('sub.tenant_id = :tenantId')
      .groupBy('sub.from_currency_id')
      .addGroupBy('sub.to_currency_id');

    return this.rateRepository
      .createQueryBuilder('rate')
      .leftJoinAndSelect('rate.fromCurrency', 'fromCurrency')
      .leftJoinAndSelect('rate.toCurrency', 'toCurrency')
      .where('rate.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere(
        `(rate.from_currency_id, rate.to_currency_id, rate.rate_date) IN (${subQuery.getQuery()})`
      )
      .setParameters({ tenantId: ctx.tenantId })
      .orderBy('fromCurrency.code', 'ASC')
      .addOrderBy('toCurrency.code', 'ASC')
      .getMany();
  }

  // ==================== CONVERSION OPERATIONS ====================

  /**
   * Convert amount between currencies
   */
  async convert(
    ctx: ServiceContext,
    amount: number,
    fromCurrencyId: string,
    toCurrencyId: string,
    date?: Date
  ): Promise<ConversionResult> {
    // Same currency - no conversion needed
    if (fromCurrencyId === toCurrencyId) {
      const currency = await this.findCurrencyById(fromCurrencyId);
      return {
        fromCurrency: currency?.code || fromCurrencyId,
        toCurrency: currency?.code || toCurrencyId,
        originalAmount: amount,
        convertedAmount: amount,
        rate: 1,
        rateDate: date || new Date(),
      };
    }

    // Try direct rate
    let rate = await this.getRate(ctx, fromCurrencyId, toCurrencyId, date);

    if (rate) {
      const convertedAmount = this.roundAmount(
        amount * Number(rate.rate),
        rate.toCurrency?.decimals || 2
      );

      return {
        fromCurrency: rate.fromCurrency?.code || fromCurrencyId,
        toCurrency: rate.toCurrency?.code || toCurrencyId,
        originalAmount: amount,
        convertedAmount,
        rate: Number(rate.rate),
        rateDate: rate.rateDate,
      };
    }

    // Try inverse rate
    rate = await this.getRate(ctx, toCurrencyId, fromCurrencyId, date);

    if (rate) {
      const inverseRate = 1 / Number(rate.rate);
      const toCurrency = await this.findCurrencyById(toCurrencyId);
      const convertedAmount = this.roundAmount(
        amount * inverseRate,
        toCurrency?.decimals || 2
      );

      return {
        fromCurrency: rate.toCurrency?.code || fromCurrencyId,
        toCurrency: rate.fromCurrency?.code || toCurrencyId,
        originalAmount: amount,
        convertedAmount,
        rate: inverseRate,
        rateDate: rate.rateDate,
      };
    }

    throw new Error('No exchange rate found for currency pair');
  }

  /**
   * Convert amount using currency codes
   */
  async convertByCode(
    ctx: ServiceContext,
    amount: number,
    fromCode: string,
    toCode: string,
    date?: Date
  ): Promise<ConversionResult> {
    const fromCurrency = await this.findCurrencyByCode(fromCode);
    const toCurrency = await this.findCurrencyByCode(toCode);

    if (!fromCurrency) {
      throw new Error(`Currency '${fromCode}' not found`);
    }
    if (!toCurrency) {
      throw new Error(`Currency '${toCode}' not found`);
    }

    return this.convert(ctx, amount, fromCurrency.id, toCurrency.id, date);
  }

  /**
   * Delete exchange rate
   */
  async deleteRate(ctx: ServiceContext, id: string): Promise<boolean> {
    const result = await this.rateRepository.delete({
      id,
      tenantId: ctx.tenantId,
    });
    return (result.affected || 0) > 0;
  }

  /**
   * Round amount based on currency decimals
   */
  private roundAmount(amount: number, decimals: number): number {
    const factor = Math.pow(10, decimals);
    return Math.round(amount * factor) / factor;
  }
}
