import { Repository, FindOptionsWhere, ILike, DataSource } from 'typeorm';
import { StockLevel, StockMovement } from '../entities';
import {
  CreateStockMovementDto,
  AdjustStockDto,
  TransferStockDto,
  ReserveStockDto,
} from '../dto';

export interface StockSearchParams {
  tenantId: string;
  productId?: string;
  warehouseId?: string;
  locationId?: string;
  lotNumber?: string;
  hasStock?: boolean;
  lowStock?: boolean;
  limit?: number;
  offset?: number;
}

export interface MovementSearchParams {
  tenantId: string;
  movementType?: string;
  productId?: string;
  warehouseId?: string;
  status?: 'draft' | 'confirmed' | 'cancelled';
  referenceType?: string;
  referenceId?: string;
  fromDate?: Date;
  toDate?: Date;
  limit?: number;
  offset?: number;
}

export class InventoryService {
  constructor(
    private readonly stockLevelRepository: Repository<StockLevel>,
    private readonly movementRepository: Repository<StockMovement>,
    private readonly dataSource: DataSource
  ) {}

  // ==================== Stock Levels ====================

  async getStockLevels(
    params: StockSearchParams
  ): Promise<{ data: StockLevel[]; total: number }> {
    const {
      tenantId,
      productId,
      warehouseId,
      locationId,
      lotNumber,
      hasStock,
      lowStock,
      limit = 50,
      offset = 0,
    } = params;

    const qb = this.stockLevelRepository
      .createQueryBuilder('stock')
      .where('stock.tenant_id = :tenantId', { tenantId });

    if (productId) {
      qb.andWhere('stock.product_id = :productId', { productId });
    }

    if (warehouseId) {
      qb.andWhere('stock.warehouse_id = :warehouseId', { warehouseId });
    }

    if (locationId) {
      qb.andWhere('stock.location_id = :locationId', { locationId });
    }

    if (lotNumber) {
      qb.andWhere('stock.lot_number = :lotNumber', { lotNumber });
    }

    if (hasStock) {
      qb.andWhere('stock.quantity_on_hand > 0');
    }

    if (lowStock) {
      qb.andWhere('stock.quantity_on_hand <= 0');
    }

    const [data, total] = await qb
      .orderBy('stock.product_id', 'ASC')
      .addOrderBy('stock.warehouse_id', 'ASC')
      .take(limit)
      .skip(offset)
      .getManyAndCount();

    return { data, total };
  }

  async getStockByProduct(
    productId: string,
    tenantId: string
  ): Promise<StockLevel[]> {
    return this.stockLevelRepository.find({
      where: { productId, tenantId },
      order: { warehouseId: 'ASC' },
    });
  }

  async getStockByWarehouse(
    warehouseId: string,
    tenantId: string
  ): Promise<StockLevel[]> {
    return this.stockLevelRepository.find({
      where: { warehouseId, tenantId },
      order: { productId: 'ASC' },
    });
  }

  async getAvailableStock(
    productId: string,
    warehouseId: string,
    tenantId: string
  ): Promise<number> {
    const stock = await this.stockLevelRepository.findOne({
      where: { productId, warehouseId, tenantId },
    });
    return stock?.quantityAvailable ?? 0;
  }

  // ==================== Stock Movements ====================

  async getMovements(
    params: MovementSearchParams
  ): Promise<{ data: StockMovement[]; total: number }> {
    const {
      tenantId,
      movementType,
      productId,
      warehouseId,
      status,
      referenceType,
      referenceId,
      fromDate,
      toDate,
      limit = 50,
      offset = 0,
    } = params;

    const qb = this.movementRepository
      .createQueryBuilder('movement')
      .where('movement.tenant_id = :tenantId', { tenantId });

    if (movementType) {
      qb.andWhere('movement.movement_type = :movementType', { movementType });
    }

    if (productId) {
      qb.andWhere('movement.product_id = :productId', { productId });
    }

    if (warehouseId) {
      qb.andWhere(
        '(movement.source_warehouse_id = :warehouseId OR movement.dest_warehouse_id = :warehouseId)',
        { warehouseId }
      );
    }

    if (status) {
      qb.andWhere('movement.status = :status', { status });
    }

    if (referenceType) {
      qb.andWhere('movement.reference_type = :referenceType', { referenceType });
    }

    if (referenceId) {
      qb.andWhere('movement.reference_id = :referenceId', { referenceId });
    }

    if (fromDate) {
      qb.andWhere('movement.created_at >= :fromDate', { fromDate });
    }

    if (toDate) {
      qb.andWhere('movement.created_at <= :toDate', { toDate });
    }

    const [data, total] = await qb
      .orderBy('movement.created_at', 'DESC')
      .take(limit)
      .skip(offset)
      .getManyAndCount();

    return { data, total };
  }

  async getMovement(id: string, tenantId: string): Promise<StockMovement | null> {
    return this.movementRepository.findOne({ where: { id, tenantId } });
  }

  async createMovement(
    tenantId: string,
    dto: CreateStockMovementDto,
    createdBy?: string
  ): Promise<StockMovement> {
    // Generate movement number
    const count = await this.movementRepository.count({ where: { tenantId } });
    const movementNumber = `MOV-${String(count + 1).padStart(6, '0')}`;

    const totalCost = dto.unitCost ? dto.unitCost * dto.quantity : undefined;

    const movement = this.movementRepository.create({
      ...dto,
      tenantId,
      movementNumber,
      totalCost,
      expiryDate: dto.expiryDate ? new Date(dto.expiryDate) : undefined,
      createdBy,
    });

    return this.movementRepository.save(movement);
  }

  async confirmMovement(
    id: string,
    tenantId: string,
    confirmedBy: string
  ): Promise<StockMovement | null> {
    const movement = await this.getMovement(id, tenantId);
    if (!movement) return null;

    if (movement.status !== 'draft') {
      throw new Error('Only draft movements can be confirmed');
    }

    // Update stock levels based on movement type
    await this.applyMovementToStock(movement);

    movement.status = 'confirmed';
    movement.confirmedAt = new Date();
    movement.confirmedBy = confirmedBy;

    return this.movementRepository.save(movement);
  }

  async cancelMovement(id: string, tenantId: string): Promise<StockMovement | null> {
    const movement = await this.getMovement(id, tenantId);
    if (!movement) return null;

    if (movement.status === 'confirmed') {
      throw new Error('Cannot cancel confirmed movement');
    }

    movement.status = 'cancelled';
    return this.movementRepository.save(movement);
  }

  // ==================== Stock Operations ====================

  async adjustStock(
    tenantId: string,
    dto: AdjustStockDto,
    userId?: string
  ): Promise<StockMovement> {
    const currentStock = await this.getStockLevel(
      dto.productId,
      dto.warehouseId,
      dto.locationId,
      dto.lotNumber,
      dto.serialNumber,
      tenantId
    );

    const currentQuantity = currentStock?.quantityOnHand ?? 0;
    const difference = dto.newQuantity - currentQuantity;

    const movement = await this.createMovement(
      tenantId,
      {
        movementType: 'adjustment',
        productId: dto.productId,
        destWarehouseId: dto.warehouseId,
        destLocationId: dto.locationId,
        quantity: Math.abs(difference),
        lotNumber: dto.lotNumber,
        serialNumber: dto.serialNumber,
        reason: dto.reason,
        notes: dto.notes,
      },
      userId
    );

    return this.confirmMovement(movement.id, tenantId, userId || '') as Promise<StockMovement>;
  }

  async transferStock(
    tenantId: string,
    dto: TransferStockDto,
    userId?: string
  ): Promise<StockMovement> {
    // Verify available stock
    const available = await this.getAvailableStock(
      dto.productId,
      dto.sourceWarehouseId,
      tenantId
    );

    if (available < dto.quantity) {
      throw new Error('Insufficient stock for transfer');
    }

    const movement = await this.createMovement(
      tenantId,
      {
        movementType: 'transfer',
        productId: dto.productId,
        sourceWarehouseId: dto.sourceWarehouseId,
        sourceLocationId: dto.sourceLocationId,
        destWarehouseId: dto.destWarehouseId,
        destLocationId: dto.destLocationId,
        quantity: dto.quantity,
        lotNumber: dto.lotNumber,
        serialNumber: dto.serialNumber,
        notes: dto.notes,
      },
      userId
    );

    return this.confirmMovement(movement.id, tenantId, userId || '') as Promise<StockMovement>;
  }

  async reserveStock(tenantId: string, dto: ReserveStockDto): Promise<boolean> {
    const stock = await this.getStockLevel(
      dto.productId,
      dto.warehouseId,
      dto.locationId,
      dto.lotNumber,
      undefined,
      tenantId
    );

    if (!stock || stock.quantityAvailable < dto.quantity) {
      throw new Error('Insufficient available stock for reservation');
    }

    stock.quantityReserved = Number(stock.quantityReserved) + dto.quantity;
    await this.stockLevelRepository.save(stock);

    return true;
  }

  async releaseReservation(
    productId: string,
    warehouseId: string,
    quantity: number,
    tenantId: string
  ): Promise<boolean> {
    const stock = await this.stockLevelRepository.findOne({
      where: { productId, warehouseId, tenantId },
    });

    if (!stock) return false;

    stock.quantityReserved = Math.max(0, Number(stock.quantityReserved) - quantity);
    await this.stockLevelRepository.save(stock);

    return true;
  }

  // ==================== Private Methods ====================

  private async getStockLevel(
    productId: string,
    warehouseId: string,
    locationId: string | undefined,
    lotNumber: string | undefined,
    serialNumber: string | undefined,
    tenantId: string
  ): Promise<StockLevel | null> {
    const where: FindOptionsWhere<StockLevel> = {
      productId,
      warehouseId,
      tenantId,
    };

    if (locationId) where.locationId = locationId;
    if (lotNumber) where.lotNumber = lotNumber;
    if (serialNumber) where.serialNumber = serialNumber;

    return this.stockLevelRepository.findOne({ where });
  }

  private async applyMovementToStock(movement: StockMovement): Promise<void> {
    const { movementType, productId, quantity, sourceWarehouseId, destWarehouseId, lotNumber } =
      movement;

    // Decrease source stock
    if (sourceWarehouseId && ['shipment', 'transfer', 'consumption'].includes(movementType)) {
      await this.updateStockLevel(
        productId,
        sourceWarehouseId,
        movement.sourceLocationId,
        lotNumber,
        movement.serialNumber,
        movement.tenantId,
        -quantity
      );
    }

    // Increase destination stock
    if (destWarehouseId && ['receipt', 'transfer', 'adjustment', 'return', 'production'].includes(movementType)) {
      await this.updateStockLevel(
        productId,
        destWarehouseId,
        movement.destLocationId,
        lotNumber,
        movement.serialNumber,
        movement.tenantId,
        quantity,
        movement.unitCost
      );
    }
  }

  private async updateStockLevel(
    productId: string,
    warehouseId: string,
    locationId: string | null,
    lotNumber: string | null,
    serialNumber: string | null,
    tenantId: string,
    quantityChange: number,
    unitCost?: number
  ): Promise<void> {
    let stock = await this.stockLevelRepository.findOne({
      where: {
        productId,
        warehouseId,
        locationId: locationId || undefined,
        lotNumber: lotNumber || undefined,
        serialNumber: serialNumber || undefined,
        tenantId,
      },
    });

    if (!stock) {
      stock = this.stockLevelRepository.create({
        productId,
        warehouseId,
        locationId: locationId || undefined,
        lotNumber: lotNumber || undefined,
        serialNumber: serialNumber || undefined,
        tenantId,
        quantityOnHand: 0,
        quantityReserved: 0,
        quantityIncoming: 0,
        quantityOutgoing: 0,
      } as Partial<StockLevel>);
    }

    stock.quantityOnHand = Number(stock.quantityOnHand) + quantityChange;
    stock.lastMovementAt = new Date();

    if (unitCost !== undefined) {
      stock.unitCost = unitCost;
      stock.totalCost = stock.quantityOnHand * unitCost;
    }

    await this.stockLevelRepository.save(stock);
  }
}
