import { Repository } from 'typeorm';
import { AppDataSource } from '../../config/typeorm.js';
import { Lot } from './entities/lot.entity.js';
import { StockQuant } from './entities/stock-quant.entity.js';
import { StockMove } from './entities/stock-move.entity.js';
import { NotFoundError, ConflictError } from '../../shared/errors/index.js';
import { logger } from '../../shared/utils/logger.js';

// ===== Interfaces =====

export interface CreateLotDto {
  productId: string;
  name: string;
  ref?: string;
  manufactureDate?: string;
  expirationDate?: string;
  removalDate?: string;
  alertDate?: string;
  notes?: string;
}

export interface UpdateLotDto {
  ref?: string | null;
  manufactureDate?: string | null;
  expirationDate?: string | null;
  removalDate?: string | null;
  alertDate?: string | null;
  notes?: string | null;
}

export interface LotFilters {
  productId?: string;
  expiringSoon?: boolean;
  expired?: boolean;
  search?: string;
  page?: number;
  limit?: number;
}

export interface LotWithRelations extends Lot {
  productName?: string | null;
  productCode?: string | null;
  quantityOnHand?: number;
}

export interface LotMovement {
  id: string;
  date: Date | null;
  origin: string | null;
  locationFrom: string;
  locationTo: string;
  quantity: number;
  status: string;
}

// ===== Service Class =====

class LotsService {
  private lotRepository: Repository<Lot>;
  private stockQuantRepository: Repository<StockQuant>;
  private stockMoveRepository: Repository<StockMove>;

  constructor() {
    this.lotRepository = AppDataSource.getRepository(Lot);
    this.stockQuantRepository = AppDataSource.getRepository(StockQuant);
    this.stockMoveRepository = AppDataSource.getRepository(StockMove);
  }

  /**
   * Get all lots with filters and pagination
   */
  async findAll(
    tenantId: string,
    filters: LotFilters = {}
  ): Promise<{ data: LotWithRelations[]; total: number }> {
    try {
      const { productId, expiringSoon, expired, search, page = 1, limit = 50 } = filters;
      const skip = (page - 1) * limit;

      const queryBuilder = this.lotRepository
        .createQueryBuilder('lot')
        .leftJoinAndSelect('lot.product', 'product')
        .where('lot.tenantId = :tenantId', { tenantId });

      // Filter by product
      if (productId) {
        queryBuilder.andWhere('lot.productId = :productId', { productId });
      }

      // Filter by expiring soon (within 30 days)
      if (expiringSoon) {
        queryBuilder.andWhere('lot.expirationDate IS NOT NULL');
        queryBuilder.andWhere('lot.expirationDate <= CURRENT_DATE + INTERVAL \'30 days\'');
        queryBuilder.andWhere('lot.expirationDate > CURRENT_DATE');
      }

      // Filter by expired
      if (expired) {
        queryBuilder.andWhere('lot.expirationDate IS NOT NULL');
        queryBuilder.andWhere('lot.expirationDate < CURRENT_DATE');
      }

      // Filter by search (lot name, ref, or product name)
      if (search) {
        queryBuilder.andWhere(
          '(lot.name ILIKE :search OR lot.ref ILIKE :search OR product.name ILIKE :search)',
          { search: `%${search}%` }
        );
      }

      // Get total count
      const total = await queryBuilder.getCount();

      // Get paginated results
      const lots = await queryBuilder
        .orderBy('lot.expirationDate', 'ASC', 'NULLS LAST')
        .addOrderBy('lot.createdAt', 'DESC')
        .skip(skip)
        .take(limit)
        .getMany();

      // Get quantity on hand for each lot using a subquery
      const lotIds = lots.map((lot) => lot.id);
      let quantitiesMap: Map<string, number> = new Map();

      if (lotIds.length > 0) {
        const quantities = await this.stockQuantRepository
          .createQueryBuilder('sq')
          .select('sq.lotId', 'lotId')
          .addSelect('SUM(sq.quantity)', 'totalQty')
          .where('sq.lotId IN (:...lotIds)', { lotIds })
          .groupBy('sq.lotId')
          .getRawMany();

        quantitiesMap = new Map(quantities.map((q) => [q.lotId, parseFloat(q.totalQty) || 0]));
      }

      // Map to include relation names and quantities
      const data: LotWithRelations[] = lots.map((lot) => ({
        ...lot,
        productName: lot.product?.name,
        productCode: lot.product?.code,
        quantityOnHand: quantitiesMap.get(lot.id) || 0,
      }));

      logger.debug('Lots retrieved', { tenantId, count: data.length, total });

      return { data, total };
    } catch (error) {
      logger.error('Error retrieving lots', {
        error: (error as Error).message,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Get lot by ID
   */
  async findById(id: string, tenantId: string): Promise<LotWithRelations> {
    try {
      const lot = await this.lotRepository
        .createQueryBuilder('lot')
        .leftJoinAndSelect('lot.product', 'product')
        .where('lot.id = :id', { id })
        .andWhere('lot.tenantId = :tenantId', { tenantId })
        .getOne();

      if (!lot) {
        throw new NotFoundError('Lote no encontrado');
      }

      // Get quantity on hand
      const quantityResult = await this.stockQuantRepository
        .createQueryBuilder('sq')
        .select('SUM(sq.quantity)', 'totalQty')
        .where('sq.lotId = :lotId', { lotId: id })
        .getRawOne();

      const quantityOnHand = parseFloat(quantityResult?.totalQty) || 0;

      return {
        ...lot,
        productName: lot.product?.name ?? null,
        productCode: lot.product?.code ?? null,
        quantityOnHand,
      };
    } catch (error) {
      logger.error('Error finding lot', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Create a new lot
   */
  async create(dto: CreateLotDto, tenantId: string, userId: string): Promise<LotWithRelations> {
    try {
      // Check for unique lot name for product
      const existing = await this.lotRepository.findOne({
        where: {
          productId: dto.productId,
          name: dto.name,
        },
      });

      if (existing) {
        throw new ConflictError('Ya existe un lote con ese nombre para este producto');
      }

      // Create lot
      const lot = this.lotRepository.create({
        tenantId,
        productId: dto.productId,
        name: dto.name,
        ref: dto.ref || null,
        manufactureDate: dto.manufactureDate ? new Date(dto.manufactureDate) : null,
        expirationDate: dto.expirationDate ? new Date(dto.expirationDate) : null,
        removalDate: dto.removalDate ? new Date(dto.removalDate) : null,
        alertDate: dto.alertDate ? new Date(dto.alertDate) : null,
        notes: dto.notes || null,
        createdBy: userId,
      });

      await this.lotRepository.save(lot);

      logger.info('Lot created', {
        lotId: lot.id,
        tenantId,
        name: lot.name,
        productId: lot.productId,
        createdBy: userId,
      });

      return this.findById(lot.id, tenantId);
    } catch (error) {
      logger.error('Error creating lot', {
        error: (error as Error).message,
        tenantId,
        dto,
      });
      throw error;
    }
  }

  /**
   * Update a lot
   */
  async update(id: string, dto: UpdateLotDto, tenantId: string): Promise<LotWithRelations> {
    try {
      const existing = await this.findById(id, tenantId);

      // Update allowed fields
      if (dto.ref !== undefined) existing.ref = dto.ref;
      if (dto.manufactureDate !== undefined) {
        existing.manufactureDate = dto.manufactureDate ? new Date(dto.manufactureDate) : null;
      }
      if (dto.expirationDate !== undefined) {
        existing.expirationDate = dto.expirationDate ? new Date(dto.expirationDate) : null;
      }
      if (dto.removalDate !== undefined) {
        existing.removalDate = dto.removalDate ? new Date(dto.removalDate) : null;
      }
      if (dto.alertDate !== undefined) {
        existing.alertDate = dto.alertDate ? new Date(dto.alertDate) : null;
      }
      if (dto.notes !== undefined) existing.notes = dto.notes;

      await this.lotRepository.save(existing);

      logger.info('Lot updated', {
        lotId: id,
        tenantId,
      });

      return this.findById(id, tenantId);
    } catch (error) {
      logger.error('Error updating lot', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Get movements for a lot
   */
  async getMovements(id: string, tenantId: string): Promise<LotMovement[]> {
    try {
      await this.findById(id, tenantId);

      const moves = await this.stockMoveRepository
        .createQueryBuilder('sm')
        .leftJoinAndSelect('sm.location', 'locationFrom')
        .leftJoinAndSelect('sm.locationDest', 'locationTo')
        .where('sm.lotId = :lotId', { lotId: id })
        .andWhere('sm.status = :status', { status: 'done' })
        .orderBy('sm.date', 'DESC')
        .getMany();

      return moves.map((move) => ({
        id: move.id,
        date: move.date,
        origin: move.origin,
        locationFrom: move.location?.name || '',
        locationTo: move.locationDest?.name || '',
        quantity: move.quantityDone,
        status: move.status,
      }));
    } catch (error) {
      logger.error('Error getting lot movements', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Delete a lot
   */
  async delete(id: string, tenantId: string): Promise<void> {
    try {
      const lot = await this.findById(id, tenantId);

      // Check if lot has stock
      if (lot.quantityOnHand && lot.quantityOnHand > 0) {
        throw new ConflictError('No se puede eliminar un lote con stock');
      }

      // Check if lot is used in moves
      const movesCount = await this.stockMoveRepository
        .createQueryBuilder('sm')
        .where('sm.lotId = :lotId', { lotId: id })
        .getCount();

      if (movesCount > 0) {
        throw new ConflictError('No se puede eliminar: el lote tiene movimientos asociados');
      }

      await this.lotRepository.delete({ id, tenantId });

      logger.info('Lot deleted', {
        lotId: id,
        tenantId,
      });
    } catch (error) {
      logger.error('Error deleting lot', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Get lots expiring soon (within specified days)
   */
  async getExpiringSoon(tenantId: string, days: number = 30): Promise<LotWithRelations[]> {
    try {
      const queryBuilder = this.lotRepository
        .createQueryBuilder('lot')
        .leftJoinAndSelect('lot.product', 'product')
        .where('lot.tenantId = :tenantId', { tenantId })
        .andWhere('lot.expirationDate IS NOT NULL')
        .andWhere('lot.expirationDate <= CURRENT_DATE + INTERVAL :days DAY', { days })
        .andWhere('lot.expirationDate >= CURRENT_DATE')
        .orderBy('lot.expirationDate', 'ASC');

      const lots = await queryBuilder.getMany();

      // Get quantities
      const lotIds = lots.map((lot) => lot.id);
      let quantitiesMap: Map<string, number> = new Map();

      if (lotIds.length > 0) {
        const quantities = await this.stockQuantRepository
          .createQueryBuilder('sq')
          .select('sq.lotId', 'lotId')
          .addSelect('SUM(sq.quantity)', 'totalQty')
          .where('sq.lotId IN (:...lotIds)', { lotIds })
          .andWhere('sq.quantity > 0')
          .groupBy('sq.lotId')
          .getRawMany();

        quantitiesMap = new Map(quantities.map((q) => [q.lotId, parseFloat(q.totalQty) || 0]));
      }

      return lots
        .filter((lot) => (quantitiesMap.get(lot.id) || 0) > 0)
        .map((lot) => ({
          ...lot,
          productName: lot.product?.name,
          productCode: lot.product?.code,
          quantityOnHand: quantitiesMap.get(lot.id) || 0,
        }));
    } catch (error) {
      logger.error('Error getting expiring lots', {
        error: (error as Error).message,
        tenantId,
        days,
      });
      throw error;
    }
  }
}

// ===== Export Singleton Instance =====

export const lotsService = new LotsService();
