import { Repository, QueryRunner } from 'typeorm';
import { AppDataSource } from '../../config/typeorm.js';
import { InventoryAdjustment, AdjustmentStatus } from './entities/inventory-adjustment.entity.js';
import { InventoryAdjustmentLine } from './entities/inventory-adjustment-line.entity.js';
import { StockQuant } from './entities/stock-quant.entity.js';
import { NotFoundError, ValidationError, ConflictError } from '../../shared/errors/index.js';
import { logger } from '../../shared/utils/logger.js';

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

export interface AdjustmentLineDto {
  productId: string;
  locationId: string;
  lotId?: string;
  countedQty: number;
  uomId: string;
  notes?: string;
}

export interface AdjustmentLineResponse {
  id: string;
  adjustmentId: string;
  productId: string;
  productName?: string | null;
  productCode?: string | null;
  locationId: string;
  locationName?: string | null;
  lotId: string | null;
  lotName?: string | null;
  theoreticalQty: number;
  countedQty: number;
  differenceQty: number | null;
  uomId: string | null;
  uomName?: string | null;
  notes: string | null;
  createdAt: Date;
}

export interface CreateAdjustmentDto {
  companyId: string;
  locationId: string;
  date?: string;
  notes?: string;
  lines: AdjustmentLineDto[];
}

export interface UpdateAdjustmentDto {
  locationId?: string;
  date?: string;
  notes?: string | null;
}

export interface UpdateAdjustmentLineDto {
  countedQty?: number;
  notes?: string | null;
}

export interface AdjustmentFilters {
  companyId?: string;
  locationId?: string;
  status?: AdjustmentStatus;
  dateFrom?: string;
  dateTo?: string;
  search?: string;
  page?: number;
  limit?: number;
}

export interface AdjustmentWithRelations extends Omit<InventoryAdjustment, 'lines'> {
  companyName?: string | null;
  locationName?: string | null;
  lines?: AdjustmentLineResponse[];
}

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

class AdjustmentsService {
  private adjustmentRepository: Repository<InventoryAdjustment>;
  private adjustmentLineRepository: Repository<InventoryAdjustmentLine>;
  private stockQuantRepository: Repository<StockQuant>;

  constructor() {
    this.adjustmentRepository = AppDataSource.getRepository(InventoryAdjustment);
    this.adjustmentLineRepository = AppDataSource.getRepository(InventoryAdjustmentLine);
    this.stockQuantRepository = AppDataSource.getRepository(StockQuant);
  }

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

      const queryBuilder = this.adjustmentRepository
        .createQueryBuilder('adjustment')
        .leftJoinAndSelect('adjustment.company', 'company')
        .leftJoinAndSelect('adjustment.location', 'location')
        .where('adjustment.tenantId = :tenantId', { tenantId });

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

      // Filter by location
      if (locationId) {
        queryBuilder.andWhere('adjustment.locationId = :locationId', { locationId });
      }

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

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

      if (dateTo) {
        queryBuilder.andWhere('adjustment.date <= :dateTo', { dateTo });
      }

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

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

      // Get paginated results
      const adjustments = await queryBuilder
        .orderBy('adjustment.date', 'DESC')
        .addOrderBy('adjustment.createdAt', 'DESC')
        .skip(skip)
        .take(limit)
        .getMany();

      // Map to include relation names for backward compatibility
      const data: AdjustmentWithRelations[] = adjustments.map((adj) => ({
        ...adj,
        companyName: adj.company?.name,
        locationName: adj.location?.name,
      }));

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

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

  /**
   * Get adjustment by ID with lines
   */
  async findById(id: string, tenantId: string): Promise<AdjustmentWithRelations> {
    try {
      // Get adjustment with basic relations
      const adjustment = await this.adjustmentRepository
        .createQueryBuilder('adjustment')
        .leftJoinAndSelect('adjustment.company', 'company')
        .leftJoinAndSelect('adjustment.location', 'location')
        .where('adjustment.id = :id', { id })
        .andWhere('adjustment.tenantId = :tenantId', { tenantId })
        .getOne();

      if (!adjustment) {
        throw new NotFoundError('Ajuste de inventario no encontrado');
      }

      // Get lines with relations
      const lines = await this.adjustmentLineRepository
        .createQueryBuilder('line')
        .leftJoinAndSelect('line.product', 'product')
        .leftJoinAndSelect('line.location', 'location')
        .leftJoinAndSelect('line.lot', 'lot')
        .leftJoin('core.uom', 'uom', 'line.uomId = uom.id')
        .addSelect(['uom.name'])
        .where('line.adjustmentId = :adjustmentId', { adjustmentId: id })
        .orderBy('line.createdAt', 'ASC')
        .getRawAndEntities();

      // Map lines to response format
      const linesResponse: AdjustmentLineResponse[] = lines.entities.map((line, index) => ({
        id: line.id,
        adjustmentId: line.adjustmentId,
        productId: line.productId,
        productName: line.product?.name,
        productCode: line.product?.code,
        locationId: line.locationId,
        locationName: line.location?.name,
        lotId: line.lotId,
        lotName: line.lot?.name || null,
        theoreticalQty: Number(line.theoreticalQty),
        countedQty: Number(line.countedQty),
        differenceQty: Number(line.differenceQty),
        uomId: line.uomId,
        uomName: lines.raw[index]?.uom_name || null,
        notes: line.notes,
        createdAt: line.createdAt,
      }));

      return {
        ...adjustment,
        companyName: adjustment.company?.name,
        locationName: adjustment.location?.name,
        lines: linesResponse,
      };
    } catch (error) {
      logger.error('Error finding adjustment', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Create a new adjustment with lines (transaction)
   */
  async create(dto: CreateAdjustmentDto, tenantId: string, userId: string): Promise<AdjustmentWithRelations> {
    // Validation
    if (!dto.lines || dto.lines.length === 0) {
      throw new ValidationError('El ajuste debe tener al menos una línea');
    }

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

    try {
      // Generate adjustment name (sequential)
      const maxNameResult = await queryRunner.manager
        .createQueryBuilder()
        .select("COALESCE(MAX(CAST(SUBSTRING(name FROM 5) AS INTEGER)), 0) + 1", "nextNum")
        .from(InventoryAdjustment, 'adj')
        .where('adj.tenantId = :tenantId', { tenantId })
        .andWhere("adj.name LIKE 'ADJ-%'")
        .getRawOne();

      const nextNum = maxNameResult?.nextNum || 1;
      const adjustmentName = `ADJ-${String(nextNum).padStart(6, '0')}`;

      const adjustmentDate = dto.date ? new Date(dto.date) : new Date();

      // Create adjustment
      const adjustment = queryRunner.manager.create(InventoryAdjustment, {
        tenantId,
        companyId: dto.companyId,
        name: adjustmentName,
        locationId: dto.locationId,
        date: adjustmentDate,
        notes: dto.notes || null,
        status: AdjustmentStatus.DRAFT,
        createdBy: userId,
      });

      const savedAdjustment = await queryRunner.manager.save(InventoryAdjustment, adjustment);

      // Create lines with theoretical qty from stock_quants
      for (const lineDto of dto.lines) {
        // Get theoretical quantity from stock_quants
        const stockResult = await queryRunner.manager
          .createQueryBuilder(StockQuant, 'sq')
          .select('COALESCE(SUM(sq.quantity), 0)', 'qty')
          .where('sq.productId = :productId', { productId: lineDto.productId })
          .andWhere('sq.locationId = :locationId', { locationId: lineDto.locationId })
          .andWhere(
            lineDto.lotId ? 'sq.lotId = :lotId' : 'sq.lotId IS NULL',
            lineDto.lotId ? { lotId: lineDto.lotId } : {}
          )
          .getRawOne();

        const theoreticalQty = parseFloat(stockResult?.qty || '0');

        const line = queryRunner.manager.create(InventoryAdjustmentLine, {
          adjustmentId: savedAdjustment.id,
          tenantId,
          productId: lineDto.productId,
          locationId: lineDto.locationId,
          lotId: lineDto.lotId || null,
          theoreticalQty,
          countedQty: lineDto.countedQty,
          uomId: lineDto.uomId || null,
          notes: lineDto.notes || null,
        });

        await queryRunner.manager.save(InventoryAdjustmentLine, line);
      }

      await queryRunner.commitTransaction();

      logger.info('Adjustment created', {
        adjustmentId: savedAdjustment.id,
        tenantId,
        name: savedAdjustment.name,
        linesCount: dto.lines.length,
        createdBy: userId,
      });

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

  /**
   * Update an adjustment (only draft status)
   */
  async update(
    id: string,
    dto: UpdateAdjustmentDto,
    tenantId: string,
    userId: string
  ): Promise<AdjustmentWithRelations> {
    try {
      const existing = await this.findById(id, tenantId);

      if (existing.status !== AdjustmentStatus.DRAFT) {
        throw new ValidationError('Solo se pueden editar ajustes en estado borrador');
      }

      // Check if there are fields to update
      const hasUpdates =
        dto.locationId !== undefined || dto.date !== undefined || dto.notes !== undefined;

      if (!hasUpdates) {
        return existing;
      }

      // Update allowed fields
      if (dto.locationId !== undefined) existing.locationId = dto.locationId;
      if (dto.date !== undefined) existing.date = new Date(dto.date);
      if (dto.notes !== undefined) existing.notes = dto.notes;

      existing.updatedBy = userId;
      existing.updatedAt = new Date();

      await this.adjustmentRepository.save(existing);

      logger.info('Adjustment updated', {
        adjustmentId: id,
        tenantId,
        updatedBy: userId,
      });

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

  /**
   * Add a new line to an adjustment
   */
  async addLine(
    adjustmentId: string,
    dto: AdjustmentLineDto,
    tenantId: string
  ): Promise<AdjustmentLineResponse> {
    try {
      const adjustment = await this.findById(adjustmentId, tenantId);

      if (adjustment.status !== AdjustmentStatus.DRAFT) {
        throw new ValidationError('Solo se pueden agregar líneas a ajustes en estado borrador');
      }

      // Get theoretical quantity from stock_quants
      const stockResult = await this.stockQuantRepository
        .createQueryBuilder('sq')
        .select('COALESCE(SUM(sq.quantity), 0)', 'qty')
        .where('sq.productId = :productId', { productId: dto.productId })
        .andWhere('sq.locationId = :locationId', { locationId: dto.locationId })
        .andWhere(
          dto.lotId ? 'sq.lotId = :lotId' : 'sq.lotId IS NULL',
          dto.lotId ? { lotId: dto.lotId } : {}
        )
        .getRawOne();

      const theoreticalQty = parseFloat(stockResult?.qty || '0');

      // Create line
      const line = this.adjustmentLineRepository.create({
        adjustmentId,
        tenantId,
        productId: dto.productId,
        locationId: dto.locationId,
        lotId: dto.lotId || null,
        theoreticalQty,
        countedQty: dto.countedQty,
        uomId: dto.uomId || null,
        notes: dto.notes || null,
      });

      const savedLine = await this.adjustmentLineRepository.save(line);

      logger.info('Adjustment line added', {
        adjustmentId,
        lineId: savedLine.id,
        tenantId,
      });

      // Return full line with relations
      const fullLine = await this.adjustmentLineRepository
        .createQueryBuilder('line')
        .leftJoinAndSelect('line.product', 'product')
        .leftJoinAndSelect('line.location', 'location')
        .leftJoinAndSelect('line.lot', 'lot')
        .leftJoin('core.uom', 'uom', 'line.uomId = uom.id')
        .addSelect(['uom.name'])
        .where('line.id = :id', { id: savedLine.id })
        .getRawAndEntities();

      const entity = fullLine.entities[0];
      const raw = fullLine.raw[0];

      return {
        id: entity.id,
        adjustmentId: entity.adjustmentId,
        productId: entity.productId,
        productName: entity.product?.name ?? null,
        productCode: entity.product?.code ?? null,
        locationId: entity.locationId,
        locationName: entity.location?.name ?? null,
        lotId: entity.lotId,
        lotName: entity.lot?.name ?? null,
        theoreticalQty: Number(entity.theoreticalQty),
        countedQty: Number(entity.countedQty),
        differenceQty: entity.differenceQty != null ? Number(entity.differenceQty) : null,
        uomId: entity.uomId,
        uomName: raw?.uom_name ?? null,
        notes: entity.notes,
        createdAt: entity.createdAt,
      };
    } catch (error) {
      logger.error('Error adding adjustment line', {
        error: (error as Error).message,
        adjustmentId,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Update an adjustment line
   */
  async updateLine(
    adjustmentId: string,
    lineId: string,
    dto: UpdateAdjustmentLineDto,
    tenantId: string
  ): Promise<AdjustmentLineResponse> {
    try {
      const adjustment = await this.findById(adjustmentId, tenantId);

      if (adjustment.status !== AdjustmentStatus.DRAFT) {
        throw new ValidationError('Solo se pueden editar líneas en ajustes en estado borrador');
      }

      const existingLine = adjustment.lines?.find((l) => l.id === lineId);
      if (!existingLine) {
        throw new NotFoundError('Línea no encontrada');
      }

      // Check if there are fields to update
      const hasUpdates = dto.countedQty !== undefined || dto.notes !== undefined;

      if (!hasUpdates) {
        return existingLine;
      }

      // Get the entity
      const line = await this.adjustmentLineRepository.findOne({
        where: { id: lineId, adjustmentId },
      });

      if (!line) {
        throw new NotFoundError('Línea no encontrada');
      }

      // Update allowed fields
      if (dto.countedQty !== undefined) line.countedQty = dto.countedQty;
      if (dto.notes !== undefined) line.notes = dto.notes;

      await this.adjustmentLineRepository.save(line);

      logger.info('Adjustment line updated', {
        adjustmentId,
        lineId,
        tenantId,
      });

      // Return updated line with relations
      const fullLine = await this.adjustmentLineRepository
        .createQueryBuilder('line')
        .leftJoinAndSelect('line.product', 'product')
        .leftJoinAndSelect('line.location', 'location')
        .leftJoinAndSelect('line.lot', 'lot')
        .leftJoin('core.uom', 'uom', 'line.uomId = uom.id')
        .addSelect(['uom.name'])
        .where('line.id = :id', { id: lineId })
        .getRawAndEntities();

      const entity = fullLine.entities[0];
      const raw = fullLine.raw[0];

      return {
        id: entity.id,
        adjustmentId: entity.adjustmentId,
        productId: entity.productId,
        productName: entity.product?.name ?? null,
        productCode: entity.product?.code ?? null,
        locationId: entity.locationId,
        locationName: entity.location?.name ?? null,
        lotId: entity.lotId,
        lotName: entity.lot?.name ?? null,
        theoreticalQty: Number(entity.theoreticalQty),
        countedQty: Number(entity.countedQty),
        differenceQty: entity.differenceQty != null ? Number(entity.differenceQty) : null,
        uomId: entity.uomId,
        uomName: raw?.uom_name ?? null,
        notes: entity.notes,
        createdAt: entity.createdAt,
      };
    } catch (error) {
      logger.error('Error updating adjustment line', {
        error: (error as Error).message,
        adjustmentId,
        lineId,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Remove a line from an adjustment
   */
  async removeLine(adjustmentId: string, lineId: string, tenantId: string): Promise<void> {
    try {
      const adjustment = await this.findById(adjustmentId, tenantId);

      if (adjustment.status !== AdjustmentStatus.DRAFT) {
        throw new ValidationError('Solo se pueden eliminar líneas en ajustes en estado borrador');
      }

      const existingLine = adjustment.lines?.find((l) => l.id === lineId);
      if (!existingLine) {
        throw new NotFoundError('Línea no encontrada');
      }

      if (adjustment.lines && adjustment.lines.length <= 1) {
        throw new ValidationError('El ajuste debe tener al menos una línea');
      }

      await this.adjustmentLineRepository.delete({ id: lineId });

      logger.info('Adjustment line removed', {
        adjustmentId,
        lineId,
        tenantId,
      });
    } catch (error) {
      logger.error('Error removing adjustment line', {
        error: (error as Error).message,
        adjustmentId,
        lineId,
        tenantId,
      });
      throw error;
    }
  }

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

      if (adjustment.status !== AdjustmentStatus.DRAFT) {
        throw new ValidationError('Solo se pueden confirmar ajustes en estado borrador');
      }

      if (!adjustment.lines || adjustment.lines.length === 0) {
        throw new ValidationError('El ajuste debe tener al menos una línea');
      }

      // Update adjustment status
      await this.adjustmentRepository
        .createQueryBuilder()
        .update(InventoryAdjustment)
        .set({
          status: AdjustmentStatus.CONFIRMED,
          updatedAt: new Date(),
          updatedBy: userId,
        })
        .where('id = :id', { id })
        .andWhere('tenantId = :tenantId', { tenantId })
        .execute();

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

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

  /**
   * Validate an adjustment (applies stock changes) - transaction
   */
  async validate(id: string, tenantId: string, userId: string): Promise<AdjustmentWithRelations> {
    const adjustment = await this.findById(id, tenantId);

    if (adjustment.status !== AdjustmentStatus.CONFIRMED) {
      throw new ValidationError('Solo se pueden validar ajustes confirmados');
    }

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

    try {
      // Update status to done
      await queryRunner.manager
        .createQueryBuilder()
        .update(InventoryAdjustment)
        .set({
          status: AdjustmentStatus.DONE,
          updatedAt: new Date(),
          updatedBy: userId,
        })
        .where('id = :id', { id })
        .andWhere('tenantId = :tenantId', { tenantId })
        .execute();

      // Apply stock adjustments for each line
      for (const line of adjustment.lines!) {
        const difference = line.countedQty - line.theoreticalQty;

        if (difference !== 0) {
          // Check if quant exists
          const existingQuant = await queryRunner.manager
            .createQueryBuilder(StockQuant, 'sq')
            .where('sq.productId = :productId', { productId: line.productId })
            .andWhere('sq.locationId = :locationId', { locationId: line.locationId })
            .andWhere(
              line.lotId ? 'sq.lotId = :lotId' : 'sq.lotId IS NULL',
              line.lotId ? { lotId: line.lotId } : {}
            )
            .getOne();

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

      await queryRunner.commitTransaction();

      logger.info('Adjustment validated', {
        adjustmentId: id,
        tenantId,
        linesCount: adjustment.lines?.length || 0,
        validatedBy: userId,
      });

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

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

      if (adjustment.status === AdjustmentStatus.DONE) {
        throw new ValidationError('No se puede cancelar un ajuste validado');
      }

      if (adjustment.status === AdjustmentStatus.CANCELLED) {
        throw new ValidationError('El ajuste ya está cancelado');
      }

      // Update adjustment status
      await this.adjustmentRepository
        .createQueryBuilder()
        .update(InventoryAdjustment)
        .set({
          status: AdjustmentStatus.CANCELLED,
          updatedAt: new Date(),
          updatedBy: userId,
        })
        .where('id = :id', { id })
        .andWhere('tenantId = :tenantId', { tenantId })
        .execute();

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

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

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

      if (adjustment.status !== AdjustmentStatus.DRAFT) {
        throw new ValidationError('Solo se pueden eliminar ajustes en estado borrador');
      }

      // Delete adjustment (cascade will delete lines)
      await this.adjustmentRepository.delete({ id, tenantId });

      logger.info('Adjustment deleted', {
        adjustmentId: id,
        tenantId,
      });
    } catch (error) {
      logger.error('Error deleting adjustment', {
        error: (error as Error).message,
        id,
        tenantId,
      });
      throw error;
    }
  }
}

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

export const adjustmentsService = new AdjustmentsService();

// Re-export AdjustmentStatus for backward compatibility
export { AdjustmentStatus };
