import { Repository, QueryRunner } from 'typeorm';
import { AppDataSource } from '../../config/typeorm.js';
import { Picking, PickingType, MoveStatus } from './entities/picking.entity.js';
import { StockMove } from './entities/stock-move.entity.js';
import { StockQuant } from './entities/stock-quant.entity.js';
import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js';
import { logger } from '../../shared/utils/logger.js';

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

export interface StockMoveLineDto {
  productId: string;
  productUomId: string;
  productQty: number;
  lotId?: string;
  locationId: string;
  locationDestId: string;
}

export interface StockMoveLineResponse {
  id: string;
  productId: string;
  productName?: string | null;
  productCode?: string | null;
  productUomId: string;
  uomName?: string | null;
  productQty: number;
  quantityDone: number;
  lotId: string | null;
  locationId: string;
  locationName?: string | null;
  locationDestId: string;
  locationDestName?: string | null;
  status: MoveStatus;
}

export interface CreatePickingDto {
  companyId: string;
  name: string;
  pickingType: PickingType;
  locationId: string;
  locationDestId: string;
  partnerId?: string;
  scheduledDate?: string;
  origin?: string;
  notes?: string;
  moves: StockMoveLineDto[];
}

export interface UpdatePickingDto {
  partnerId?: string | null;
  scheduledDate?: string | null;
  origin?: string | null;
  notes?: string | null;
  moves?: StockMoveLineDto[];
}

export interface PickingFilters {
  companyId?: string;
  pickingType?: PickingType;
  status?: MoveStatus;
  partnerId?: string;
  dateFrom?: string;
  dateTo?: string;
  search?: string;
  page?: number;
  limit?: number;
}

export interface PickingWithRelations extends Omit<Picking, 'moves'> {
  companyName?: string | null;
  locationName?: string | null;
  locationDestName?: string | null;
  partnerName?: string | null;
  moves?: StockMoveLineResponse[];
}

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

class PickingsService {
  private pickingRepository: Repository<Picking>;
  private stockMoveRepository: Repository<StockMove>;
  private stockQuantRepository: Repository<StockQuant>;

  constructor() {
    this.pickingRepository = AppDataSource.getRepository(Picking);
    this.stockMoveRepository = AppDataSource.getRepository(StockMove);
    this.stockQuantRepository = AppDataSource.getRepository(StockQuant);
  }

  /**
   * Get all pickings with filters and pagination
   */
  async findAll(
    tenantId: string,
    filters: PickingFilters = {}
  ): Promise<{ data: PickingWithRelations[]; total: number }> {
    try {
      const {
        companyId,
        pickingType,
        status,
        partnerId,
        dateFrom,
        dateTo,
        search,
        page = 1,
        limit = 20,
      } = filters;
      const skip = (page - 1) * limit;

      const queryBuilder = this.pickingRepository
        .createQueryBuilder('picking')
        .leftJoinAndSelect('picking.company', 'company')
        .leftJoinAndSelect('picking.location', 'location')
        .leftJoinAndSelect('picking.locationDest', 'locationDest')
        .leftJoin('core.partners', 'partner', 'picking.partnerId = partner.id')
        .addSelect(['partner.name'])
        .where('picking.tenantId = :tenantId', { tenantId });

      // Filter by company
      if (companyId) {
        queryBuilder.andWhere('picking.companyId = :companyId', { companyId });
      }

      // Filter by picking type
      if (pickingType) {
        queryBuilder.andWhere('picking.pickingType = :pickingType', { pickingType });
      }

      // Filter by status
      if (status) {
        queryBuilder.andWhere('picking.status = :status', { status });
      }

      // Filter by partner
      if (partnerId) {
        queryBuilder.andWhere('picking.partnerId = :partnerId', { partnerId });
      }

      // Filter by date range
      if (dateFrom) {
        queryBuilder.andWhere('picking.scheduledDate >= :dateFrom', { dateFrom });
      }

      if (dateTo) {
        queryBuilder.andWhere('picking.scheduledDate <= :dateTo', { dateTo });
      }

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

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

      // Get paginated results
      const pickings = await queryBuilder
        .orderBy('picking.scheduledDate', 'DESC', 'NULLS LAST')
        .addOrderBy('picking.name', 'DESC')
        .skip(skip)
        .take(limit)
        .getRawAndEntities();

      // Map to include relation names for backward compatibility
      const data: PickingWithRelations[] = pickings.entities.map((picking, index) => ({
        ...picking,
        companyName: picking.company?.name,
        locationName: picking.location?.name,
        locationDestName: picking.locationDest?.name,
        partnerName: pickings.raw[index]?.partner_name || null,
      }));

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

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

  /**
   * Get picking by ID with moves
   */
  async findById(id: string, tenantId: string): Promise<PickingWithRelations> {
    try {
      // Get picking with basic relations
      const pickingResult = await this.pickingRepository
        .createQueryBuilder('picking')
        .leftJoinAndSelect('picking.company', 'company')
        .leftJoinAndSelect('picking.location', 'location')
        .leftJoinAndSelect('picking.locationDest', 'locationDest')
        .leftJoin('core.partners', 'partner', 'picking.partnerId = partner.id')
        .addSelect(['partner.name'])
        .where('picking.id = :id', { id })
        .andWhere('picking.tenantId = :tenantId', { tenantId })
        .getRawAndEntities();

      const picking = pickingResult.entities[0];
      if (!picking) {
        throw new NotFoundError('Picking no encontrado');
      }

      // Get moves with relations
      const moves = await this.stockMoveRepository
        .createQueryBuilder('sm')
        .leftJoinAndSelect('sm.product', 'product')
        .leftJoinAndSelect('sm.location', 'locationFrom')
        .leftJoinAndSelect('sm.locationDest', 'locationTo')
        .leftJoin('core.uom', 'uom', 'sm.productUomId = uom.id')
        .addSelect(['uom.name'])
        .where('sm.pickingId = :pickingId', { pickingId: id })
        .orderBy('sm.createdAt', 'ASC')
        .getRawAndEntities();

      // Map moves to response format
      const movesResponse: StockMoveLineResponse[] = moves.entities.map((move, index) => ({
        id: move.id,
        productId: move.productId,
        productName: move.product?.name,
        productCode: move.product?.code,
        productUomId: move.productUomId,
        uomName: moves.raw[index]?.uom_name || null,
        productQty: Number(move.productQty),
        quantityDone: Number(move.quantityDone),
        lotId: move.lotId,
        locationId: move.locationId,
        locationName: move.location?.name,
        locationDestId: move.locationDestId,
        locationDestName: move.locationDest?.name,
        status: move.status,
      }));

      return {
        ...picking,
        companyName: picking.company?.name,
        locationName: picking.location?.name,
        locationDestName: picking.locationDest?.name,
        partnerName: pickingResult.raw[0]?.partner_name || null,
        moves: movesResponse,
      };
    } catch (error) {
      logger.error('Error finding picking', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Create a new picking with moves (transaction)
   */
  async create(dto: CreatePickingDto, tenantId: string, userId: string): Promise<PickingWithRelations> {
    // Validation
    if (!dto.moves || dto.moves.length === 0) {
      throw new ValidationError('El picking debe tener al menos un movimiento');
    }

    const queryRunner: QueryRunner = AppDataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      // Create picking
      const picking = queryRunner.manager.create(Picking, {
        tenantId,
        companyId: dto.companyId,
        name: dto.name,
        pickingType: dto.pickingType,
        locationId: dto.locationId,
        locationDestId: dto.locationDestId,
        partnerId: dto.partnerId || null,
        scheduledDate: dto.scheduledDate ? new Date(dto.scheduledDate) : null,
        origin: dto.origin || null,
        notes: dto.notes || null,
        status: MoveStatus.DRAFT,
        createdBy: userId,
      });

      const savedPicking = await queryRunner.manager.save(Picking, picking);

      // Create moves
      for (const moveDto of dto.moves) {
        const move = queryRunner.manager.create(StockMove, {
          tenantId,
          pickingId: savedPicking.id,
          productId: moveDto.productId,
          productUomId: moveDto.productUomId,
          locationId: moveDto.locationId,
          locationDestId: moveDto.locationDestId,
          productQty: moveDto.productQty,
          quantityDone: 0,
          lotId: moveDto.lotId || null,
          status: MoveStatus.DRAFT,
          createdBy: userId,
        });

        await queryRunner.manager.save(StockMove, move);
      }

      await queryRunner.commitTransaction();

      logger.info('Picking created', {
        pickingId: savedPicking.id,
        tenantId,
        name: savedPicking.name,
        movesCount: dto.moves.length,
        createdBy: userId,
      });

      return this.findById(savedPicking.id, tenantId);
    } catch (error) {
      await queryRunner.rollbackTransaction();
      logger.error('Error creating picking', {
        error: (error as Error).message,
        tenantId,
        dto,
      });
      throw error;
    } finally {
      await queryRunner.release();
    }
  }

  /**
   * Confirm a picking (draft -> confirmed)
   */
  async confirm(id: string, tenantId: string, userId: string): Promise<PickingWithRelations> {
    try {
      const picking = await this.findById(id, tenantId);

      if (picking.status !== MoveStatus.DRAFT) {
        throw new ConflictError('Solo se pueden confirmar pickings en estado borrador');
      }

      // Update picking status
      await this.pickingRepository
        .createQueryBuilder()
        .update(Picking)
        .set({
          status: MoveStatus.CONFIRMED,
          updatedAt: new Date(),
          updatedBy: userId,
        })
        .where('id = :id', { id })
        .execute();

      // Update all moves status
      await this.stockMoveRepository
        .createQueryBuilder()
        .update(StockMove)
        .set({
          status: MoveStatus.CONFIRMED,
          updatedAt: new Date(),
          updatedBy: userId,
        })
        .where('pickingId = :pickingId', { pickingId: id })
        .execute();

      logger.info('Picking confirmed', {
        pickingId: id,
        tenantId,
        confirmedBy: userId,
      });

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

  /**
   * Validate a picking (updates stock quants) - transaction
   */
  async validate(id: string, tenantId: string, userId: string): Promise<PickingWithRelations> {
    const picking = await this.findById(id, tenantId);

    if (picking.status === MoveStatus.DONE) {
      throw new ConflictError('El picking ya esta validado');
    }

    if (picking.status === MoveStatus.CANCELLED) {
      throw new ConflictError('No se puede validar un picking cancelado');
    }

    const queryRunner: QueryRunner = AppDataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      // Update stock quants for each move
      for (const move of picking.moves || []) {
        const qty = move.productQty;
        const lotIdValue = move.lotId || null;

        // Decrease from source location
        await this.updateStockQuant(
          queryRunner,
          move.productId,
          move.locationId,
          lotIdValue,
          -qty,
          tenantId
        );

        // Increase in destination location
        await this.updateStockQuant(
          queryRunner,
          move.productId,
          move.locationDestId,
          lotIdValue,
          qty,
          tenantId
        );

        // Update move to done
        await queryRunner.manager
          .createQueryBuilder()
          .update(StockMove)
          .set({
            quantityDone: qty,
            status: MoveStatus.DONE,
            date: new Date(),
            updatedAt: new Date(),
            updatedBy: userId,
          })
          .where('id = :moveId', { moveId: move.id })
          .execute();
      }

      // Update picking to done
      await queryRunner.manager
        .createQueryBuilder()
        .update(Picking)
        .set({
          status: MoveStatus.DONE,
          dateDone: new Date(),
          validatedAt: new Date(),
          validatedBy: userId,
          updatedAt: new Date(),
          updatedBy: userId,
        })
        .where('id = :id', { id })
        .execute();

      await queryRunner.commitTransaction();

      logger.info('Picking validated', {
        pickingId: id,
        tenantId,
        movesCount: picking.moves?.length || 0,
        validatedBy: userId,
      });

      return this.findById(id, tenantId);
    } catch (error) {
      await queryRunner.rollbackTransaction();
      logger.error('Error validating picking', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    } finally {
      await queryRunner.release();
    }
  }

  /**
   * Helper method to update or create stock quant
   */
  private async updateStockQuant(
    queryRunner: QueryRunner,
    productId: string,
    locationId: string,
    lotId: string | null,
    quantityDelta: number,
    tenantId: string
  ): Promise<void> {
    // Try to find existing quant
    const existingQuant = await queryRunner.manager
      .createQueryBuilder(StockQuant, 'sq')
      .where('sq.productId = :productId', { productId })
      .andWhere('sq.locationId = :locationId', { locationId })
      .andWhere(lotId ? 'sq.lotId = :lotId' : 'sq.lotId IS NULL', { lotId })
      .getOne();

    if (existingQuant) {
      // Update existing quant
      const newQuantity = Number(existingQuant.quantity) + quantityDelta;
      await queryRunner.manager
        .createQueryBuilder()
        .update(StockQuant)
        .set({
          quantity: newQuantity,
          updatedAt: new Date(),
        })
        .where('id = :id', { id: existingQuant.id })
        .execute();
    } else {
      // Create new quant
      const newQuant = queryRunner.manager.create(StockQuant, {
        tenantId,
        productId,
        locationId,
        lotId,
        quantity: quantityDelta,
        reservedQuantity: 0,
      });
      await queryRunner.manager.save(StockQuant, newQuant);
    }
  }

  /**
   * Cancel a picking
   */
  async cancel(id: string, tenantId: string, userId: string): Promise<PickingWithRelations> {
    try {
      const picking = await this.findById(id, tenantId);

      if (picking.status === MoveStatus.DONE) {
        throw new ConflictError('No se puede cancelar un picking ya validado');
      }

      if (picking.status === MoveStatus.CANCELLED) {
        throw new ConflictError('El picking ya esta cancelado');
      }

      // Update picking status
      await this.pickingRepository
        .createQueryBuilder()
        .update(Picking)
        .set({
          status: MoveStatus.CANCELLED,
          updatedAt: new Date(),
          updatedBy: userId,
        })
        .where('id = :id', { id })
        .execute();

      // Update all moves status
      await this.stockMoveRepository
        .createQueryBuilder()
        .update(StockMove)
        .set({
          status: MoveStatus.CANCELLED,
          updatedAt: new Date(),
          updatedBy: userId,
        })
        .where('pickingId = :pickingId', { pickingId: id })
        .execute();

      logger.info('Picking cancelled', {
        pickingId: id,
        tenantId,
        cancelledBy: userId,
      });

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

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

      if (picking.status !== MoveStatus.DRAFT) {
        throw new ConflictError('Solo se pueden eliminar pickings en estado borrador');
      }

      // Delete picking (cascade will delete moves)
      await this.pickingRepository.delete({ id, tenantId });

      logger.info('Picking deleted', {
        pickingId: id,
        tenantId,
      });
    } catch (error) {
      logger.error('Error deleting picking', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }
}

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

export const pickingsService = new PickingsService();

// Re-export enums for backward compatibility
export { PickingType, MoveStatus };
