import { Repository, LessThanOrEqual } from 'typeorm';
import { AppDataSource } from '../../config/typeorm.js';
import { CurrencyRate, RateSource } from './entities/currency-rate.entity.js';
import { Currency } from './entities/currency.entity.js';
import { NotFoundError } from '../../shared/errors/index.js';
import { logger } from '../../shared/utils/logger.js';

export interface CreateCurrencyRateDto {
  tenantId?: string;
  fromCurrencyCode: string;
  toCurrencyCode: string;
  rate: number;
  rateDate: Date;
  source?: RateSource;
  createdBy?: string;
}

export interface CurrencyRateFilter {
  tenantId?: string;
  fromCurrencyCode?: string;
  toCurrencyCode?: string;
  dateFrom?: Date;
  dateTo?: Date;
  limit?: number;
}

export interface ConvertCurrencyDto {
  amount: number;
  fromCurrencyCode: string;
  toCurrencyCode: string;
  date?: Date;
  tenantId?: string;
}

class CurrencyRatesService {
  private repository: Repository<CurrencyRate>;
  private currencyRepository: Repository<Currency>;

  constructor() {
    this.repository = AppDataSource.getRepository(CurrencyRate);
    this.currencyRepository = AppDataSource.getRepository(Currency);
  }

  async findAll(filter: CurrencyRateFilter = {}): Promise<CurrencyRate[]> {
    logger.debug('Finding currency rates', { filter });

    const query = this.repository
      .createQueryBuilder('rate')
      .leftJoinAndSelect('rate.fromCurrency', 'fromCurrency')
      .leftJoinAndSelect('rate.toCurrency', 'toCurrency');

    if (filter.tenantId) {
      query.andWhere('(rate.tenantId = :tenantId OR rate.tenantId IS NULL)', {
        tenantId: filter.tenantId,
      });
    }

    if (filter.fromCurrencyCode) {
      query.andWhere('fromCurrency.code = :fromCode', {
        fromCode: filter.fromCurrencyCode.toUpperCase(),
      });
    }

    if (filter.toCurrencyCode) {
      query.andWhere('toCurrency.code = :toCode', {
        toCode: filter.toCurrencyCode.toUpperCase(),
      });
    }

    if (filter.dateFrom) {
      query.andWhere('rate.rateDate >= :dateFrom', { dateFrom: filter.dateFrom });
    }

    if (filter.dateTo) {
      query.andWhere('rate.rateDate <= :dateTo', { dateTo: filter.dateTo });
    }

    query.orderBy('rate.rateDate', 'DESC');

    if (filter.limit) {
      query.take(filter.limit);
    }

    return query.getMany();
  }

  async findById(id: string): Promise<CurrencyRate> {
    logger.debug('Finding currency rate by id', { id });

    const rate = await this.repository.findOne({
      where: { id },
      relations: ['fromCurrency', 'toCurrency'],
    });

    if (!rate) {
      throw new NotFoundError('Tipo de cambio no encontrado');
    }

    return rate;
  }

  async getRate(
    fromCurrencyCode: string,
    toCurrencyCode: string,
    date: Date = new Date(),
    tenantId?: string
  ): Promise<number | null> {
    logger.debug('Getting currency rate', { fromCurrencyCode, toCurrencyCode, date, tenantId });

    // Same currency = rate 1
    if (fromCurrencyCode.toUpperCase() === toCurrencyCode.toUpperCase()) {
      return 1;
    }

    // Find direct rate
    const directRate = await this.repository
      .createQueryBuilder('rate')
      .leftJoin('rate.fromCurrency', 'fromCurrency')
      .leftJoin('rate.toCurrency', 'toCurrency')
      .where('fromCurrency.code = :fromCode', { fromCode: fromCurrencyCode.toUpperCase() })
      .andWhere('toCurrency.code = :toCode', { toCode: toCurrencyCode.toUpperCase() })
      .andWhere('rate.rateDate <= :date', { date })
      .andWhere('(rate.tenantId = :tenantId OR rate.tenantId IS NULL)', { tenantId: tenantId || null })
      .orderBy('rate.rateDate', 'DESC')
      .addOrderBy('rate.tenantId', 'DESC', 'NULLS LAST')
      .getOne();

    if (directRate) {
      return Number(directRate.rate);
    }

    // Try inverse rate
    const inverseRate = await this.repository
      .createQueryBuilder('rate')
      .leftJoin('rate.fromCurrency', 'fromCurrency')
      .leftJoin('rate.toCurrency', 'toCurrency')
      .where('fromCurrency.code = :toCode', { toCode: toCurrencyCode.toUpperCase() })
      .andWhere('toCurrency.code = :fromCode', { fromCode: fromCurrencyCode.toUpperCase() })
      .andWhere('rate.rateDate <= :date', { date })
      .andWhere('(rate.tenantId = :tenantId OR rate.tenantId IS NULL)', { tenantId: tenantId || null })
      .orderBy('rate.rateDate', 'DESC')
      .addOrderBy('rate.tenantId', 'DESC', 'NULLS LAST')
      .getOne();

    if (inverseRate) {
      return 1 / Number(inverseRate.rate);
    }

    return null;
  }

  async convert(dto: ConvertCurrencyDto): Promise<{ amount: number; rate: number } | null> {
    logger.debug('Converting currency', dto);

    const rate = await this.getRate(
      dto.fromCurrencyCode,
      dto.toCurrencyCode,
      dto.date || new Date(),
      dto.tenantId
    );

    if (rate === null) {
      return null;
    }

    return {
      amount: dto.amount * rate,
      rate,
    };
  }

  async create(dto: CreateCurrencyRateDto): Promise<CurrencyRate> {
    logger.info('Creating currency rate', { dto });

    // Get currency IDs
    const fromCurrency = await this.currencyRepository.findOne({
      where: { code: dto.fromCurrencyCode.toUpperCase() },
    });
    if (!fromCurrency) {
      throw new NotFoundError(`Moneda ${dto.fromCurrencyCode} no encontrada`);
    }

    const toCurrency = await this.currencyRepository.findOne({
      where: { code: dto.toCurrencyCode.toUpperCase() },
    });
    if (!toCurrency) {
      throw new NotFoundError(`Moneda ${dto.toCurrencyCode} no encontrada`);
    }

    // Check if rate already exists for this date
    const existing = await this.repository.findOne({
      where: {
        tenantId: dto.tenantId || undefined,
        fromCurrencyId: fromCurrency.id,
        toCurrencyId: toCurrency.id,
        rateDate: dto.rateDate,
      },
    });

    if (existing) {
      // Update existing rate
      existing.rate = dto.rate;
      existing.source = dto.source || 'manual';
      return this.repository.save(existing);
    }

    const rate = this.repository.create({
      tenantId: dto.tenantId || null,
      fromCurrencyId: fromCurrency.id,
      toCurrencyId: toCurrency.id,
      rate: dto.rate,
      rateDate: dto.rateDate,
      source: dto.source || 'manual',
      createdBy: dto.createdBy || null,
    });

    return this.repository.save(rate);
  }

  async delete(id: string): Promise<void> {
    logger.info('Deleting currency rate', { id });

    const rate = await this.findById(id);
    await this.repository.remove(rate);
  }

  async getHistory(
    fromCurrencyCode: string,
    toCurrencyCode: string,
    days: number = 30,
    tenantId?: string
  ): Promise<CurrencyRate[]> {
    logger.debug('Getting rate history', { fromCurrencyCode, toCurrencyCode, days, tenantId });

    const dateFrom = new Date();
    dateFrom.setDate(dateFrom.getDate() - days);

    return this.findAll({
      fromCurrencyCode,
      toCurrencyCode,
      dateFrom,
      tenantId,
      limit: days,
    });
  }

  async getLatestRates(baseCurrencyCode: string = 'MXN', tenantId?: string): Promise<Map<string, number>> {
    logger.debug('Getting latest rates', { baseCurrencyCode, tenantId });

    const rates = new Map<string, number>();
    const currencies = await this.currencyRepository.find({ where: { active: true } });

    for (const currency of currencies) {
      if (currency.code === baseCurrencyCode.toUpperCase()) {
        rates.set(currency.code, 1);
        continue;
      }

      const rate = await this.getRate(baseCurrencyCode, currency.code, new Date(), tenantId);
      if (rate !== null) {
        rates.set(currency.code, rate);
      }
    }

    return rates;
  }
}

export const currencyRatesService = new CurrencyRatesService();
