import { Repository } from 'typeorm';
import { AppDataSource } from '../../config/typeorm.js';
import { Uom, UomType } from './entities/uom.entity.js';
import { UomCategory } from './entities/uom-category.entity.js';
import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js';
import { logger } from '../../shared/utils/logger.js';

export interface CreateUomDto {
  name: string;
  code: string;
  category_id?: string;
  categoryId?: string; // Accept camelCase too
  uom_type?: 'reference' | 'bigger' | 'smaller';
  uomType?: 'reference' | 'bigger' | 'smaller'; // Accept camelCase too
  ratio?: number;
}

export interface UpdateUomDto {
  name?: string;
  ratio?: number;
  active?: boolean;
}

export interface CreateUomCategoryDto {
  name: string;
  description?: string;
}

export interface UpdateUomCategoryDto {
  name?: string;
  description?: string | null;
}

export interface ConversionResult {
  result: number;
  fromUom: string;
  toUom: string;
  factor: number;
}

class UomService {
  private repository: Repository<Uom>;
  private categoryRepository: Repository<UomCategory>;

  constructor() {
    this.repository = AppDataSource.getRepository(Uom);
    this.categoryRepository = AppDataSource.getRepository(UomCategory);
  }

  // Categories
  async findAllCategories(activeOnly: boolean = false): Promise<UomCategory[]> {
    logger.debug('Finding all UOM categories', { activeOnly });

    const queryBuilder = this.categoryRepository
      .createQueryBuilder('category')
      .orderBy('category.name', 'ASC');

    // Note: activeOnly is not supported since the table doesn't have an active field
    // Keeping the parameter for backward compatibility

    return queryBuilder.getMany();
  }

  async findCategoryById(id: string): Promise<UomCategory> {
    logger.debug('Finding UOM category by id', { id });

    const category = await this.categoryRepository.findOne({
      where: { id },
    });

    if (!category) {
      throw new NotFoundError('Categoría de UdM no encontrada');
    }

    return category;
  }

  // UoM
  async findAll(categoryId?: string, activeOnly: boolean = false): Promise<Uom[]> {
    logger.debug('Finding all UOMs', { categoryId, activeOnly });

    const queryBuilder = this.repository
      .createQueryBuilder('u')
      .leftJoinAndSelect('u.category', 'uc')
      .orderBy('uc.name', 'ASC')
      .addOrderBy('u.uomType', 'ASC')
      .addOrderBy('u.name', 'ASC');

    if (categoryId) {
      queryBuilder.where('u.categoryId = :categoryId', { categoryId });
    }

    if (activeOnly) {
      queryBuilder.andWhere('u.active = :active', { active: true });
    }

    return queryBuilder.getMany();
  }

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

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

    if (!uom) {
      throw new NotFoundError('Unidad de medida no encontrada');
    }

    return uom;
  }

  async create(dto: CreateUomDto): Promise<Uom> {
    logger.debug('Creating UOM', { dto });

    // Accept both snake_case and camelCase
    const categoryId = dto.category_id ?? dto.categoryId;
    const uomType = dto.uom_type ?? dto.uomType ?? 'reference';
    const factor = dto.ratio ?? 1;

    if (!categoryId) {
      throw new NotFoundError('category_id es requerido');
    }

    // Validate category exists
    await this.findCategoryById(categoryId);

    // Check unique code
    if (dto.code) {
      const existing = await this.repository.findOne({
        where: { code: dto.code },
      });

      if (existing) {
        throw new ConflictError(`Ya existe una UdM con código ${dto.code}`);
      }
    }

    const uom = this.repository.create({
      name: dto.name,
      code: dto.code,
      categoryId,
      uomType: uomType as UomType,
      factor,
    });

    const saved = await this.repository.save(uom);
    logger.info('UOM created', { id: saved.id, code: saved.code });

    return saved;
  }

  async update(id: string, dto: UpdateUomDto): Promise<Uom> {
    logger.debug('Updating UOM', { id, dto });

    const uom = await this.findById(id);

    if (dto.name !== undefined) {
      uom.name = dto.name;
    }

    if (dto.ratio !== undefined) {
      uom.factor = dto.ratio;
    }

    if (dto.active !== undefined) {
      uom.active = dto.active;
    }

    const updated = await this.repository.save(uom);
    logger.info('UOM updated', { id: updated.id, code: updated.code });

    return updated;
  }

  // ========== UOM CONVERSION METHODS ==========

  /**
   * Convert a quantity from one UoM to another
   * Both UoMs must belong to the same category
   * Formula: result = quantity * (fromUom.factor / toUom.factor)
   */
  async convert(quantity: number, fromUomId: string, toUomId: string): Promise<ConversionResult> {
    logger.debug('Converting UoM', { quantity, fromUomId, toUomId });

    const fromUom = await this.findById(fromUomId);
    const toUom = await this.findById(toUomId);

    // Validate both UoMs belong to the same category
    if (fromUom.categoryId !== toUom.categoryId) {
      throw new ValidationError(
        `No se puede convertir entre diferentes categorías: ${fromUom.category?.name || fromUom.categoryId} y ${toUom.category?.name || toUom.categoryId}`
      );
    }

    const factor = this.calculateConversionFactor(fromUom.factor, toUom.factor);
    const result = quantity * factor;

    logger.info('UoM conversion completed', {
      quantity,
      fromUom: fromUom.code || fromUom.name,
      toUom: toUom.code || toUom.name,
      factor,
      result,
    });

    return {
      result,
      fromUom: fromUom.code || fromUom.name,
      toUom: toUom.code || toUom.name,
      factor,
    };
  }

  /**
   * Convert a quantity from one UoM to another using UoM codes
   * Both UoMs must belong to the same category
   */
  async convertByCode(quantity: number, fromCode: string, toCode: string): Promise<ConversionResult> {
    logger.debug('Converting UoM by code', { quantity, fromCode, toCode });

    const fromUom = await this.findByCode(fromCode);
    const toUom = await this.findByCode(toCode);

    // Validate both UoMs belong to the same category
    if (fromUom.categoryId !== toUom.categoryId) {
      throw new ValidationError(
        `No se puede convertir entre diferentes categorías: ${fromUom.category?.name || fromUom.categoryId} y ${toUom.category?.name || toUom.categoryId}`
      );
    }

    const factor = this.calculateConversionFactor(fromUom.factor, toUom.factor);
    const result = quantity * factor;

    logger.info('UoM conversion by code completed', {
      quantity,
      fromCode,
      toCode,
      factor,
      result,
    });

    return {
      result,
      fromUom: fromUom.code || fromUom.name,
      toUom: toUom.code || toUom.name,
      factor,
    };
  }

  /**
   * Get the conversion factor between two UoMs
   * Returns the factor to multiply by for conversion
   */
  async getConversionFactor(fromUomId: string, toUomId: string): Promise<number> {
    logger.debug('Getting conversion factor', { fromUomId, toUomId });

    const fromUom = await this.findById(fromUomId);
    const toUom = await this.findById(toUomId);

    // Validate both UoMs belong to the same category
    if (fromUom.categoryId !== toUom.categoryId) {
      throw new ValidationError(
        `No se puede obtener factor de conversión entre diferentes categorías: ${fromUom.category?.name || fromUom.categoryId} y ${toUom.category?.name || toUom.categoryId}`
      );
    }

    return this.calculateConversionFactor(fromUom.factor, toUom.factor);
  }

  /**
   * Get the reference UoM for a category (uomType = 'reference')
   */
  async getReferenceUom(categoryId: string): Promise<Uom> {
    logger.debug('Getting reference UoM for category', { categoryId });

    // Validate category exists
    await this.findCategoryById(categoryId);

    const referenceUom = await this.repository.findOne({
      where: {
        categoryId,
        uomType: UomType.REFERENCE,
      },
      relations: ['category'],
    });

    if (!referenceUom) {
      throw new NotFoundError(`No se encontró UdM de referencia para la categoría ${categoryId}`);
    }

    return referenceUom;
  }

  /**
   * Find UoM by code
   */
  async findByCode(code: string): Promise<Uom> {
    logger.debug('Finding UOM by code', { code });

    const uom = await this.repository.findOne({
      where: { code },
      relations: ['category'],
    });

    if (!uom) {
      throw new NotFoundError(`Unidad de medida con código '${code}' no encontrada`);
    }

    return uom;
  }

  /**
   * Calculate conversion factor between two UoM factors
   * Formula: fromFactor / toFactor
   */
  private calculateConversionFactor(fromFactor: number, toFactor: number): number {
    // Ensure numeric values (TypeORM may return strings for decimals)
    const from = Number(fromFactor);
    const to = Number(toFactor);

    if (to === 0) {
      throw new ValidationError('El factor de la UdM destino no puede ser cero');
    }

    return from / to;
  }

  // ========== UOM CATEGORY CRUD METHODS ==========

  /**
   * Create a new UoM category
   */
  async createCategory(dto: CreateUomCategoryDto): Promise<UomCategory> {
    logger.debug('Creating UoM category', { dto });

    // Check for duplicate name
    const existing = await this.categoryRepository.findOne({
      where: { name: dto.name },
    });

    if (existing) {
      throw new ConflictError(`Ya existe una categoría de UdM con nombre '${dto.name}'`);
    }

    const category = this.categoryRepository.create({
      name: dto.name,
      description: dto.description || null,
    });

    const saved = await this.categoryRepository.save(category);
    logger.info('UoM category created', { id: saved.id, name: saved.name });

    return saved;
  }

  /**
   * Update a UoM category
   */
  async updateCategory(id: string, dto: UpdateUomCategoryDto): Promise<UomCategory> {
    logger.debug('Updating UoM category', { id, dto });

    const category = await this.findCategoryById(id);

    if (dto.name !== undefined) {
      // Check for duplicate name (excluding current category)
      const existing = await this.categoryRepository.findOne({
        where: { name: dto.name },
      });

      if (existing && existing.id !== id) {
        throw new ConflictError(`Ya existe una categoría de UdM con nombre '${dto.name}'`);
      }

      category.name = dto.name;
    }

    if (dto.description !== undefined) {
      category.description = dto.description;
    }

    const updated = await this.categoryRepository.save(category);
    logger.info('UoM category updated', { id: updated.id, name: updated.name });

    return updated;
  }

  /**
   * Delete a UoM category
   * Only allowed if no UoMs exist in the category
   */
  async deleteCategory(id: string): Promise<void> {
    logger.debug('Deleting UoM category', { id });

    const category = await this.findCategoryById(id);

    // Check if any UoMs exist in this category
    const uomCount = await this.repository.count({
      where: { categoryId: id },
    });

    if (uomCount > 0) {
      throw new ConflictError(
        `No se puede eliminar la categoría '${category.name}' porque tiene ${uomCount} unidad(es) de medida asociada(s)`
      );
    }

    await this.categoryRepository.remove(category);
    logger.info('UoM category deleted', { id, name: category.name });
  }
}

export const uomService = new UomService();
