import { Repository, QueryRunner } from 'typeorm';
import { AppDataSource } from '../../config/typeorm.js';
import { StockValuationLayer } from './entities/stock-valuation-layer.entity.js';
import { Product, ValuationMethod } from './entities/product.entity.js';
import { StockMove } from './entities/stock-move.entity.js';
import { Location } from './entities/location.entity.js';
import { Picking } from './entities/picking.entity.js';
import { NotFoundError, ValidationError } from '../../shared/errors/index.js';
import { logger } from '../../shared/utils/logger.js';

// ============================================================================
// INTERFACES
// ============================================================================

export interface CreateValuationLayerDto {
  productId: string;
  companyId: string;
  quantity: number;
  unitCost: number;
  stockMoveId?: string;
  description?: string;
}

export interface ValuationSummary {
  productId: string;
  productName: string;
  productCode?: string;
  totalQuantity: number;
  totalValue: number;
  averageCost: number;
  valuationMethod: ValuationMethod;
  layerCount: number;
}

export interface FifoConsumptionResult {
  layersConsumed: {
    layerId: string;
    quantityConsumed: number;
    unitCost: number;
    valueConsumed: number;
  }[];
  totalCost: number;
  weightedAverageCost: number;
}

export interface ProductCostResult {
  productId: string;
  valuationMethod: ValuationMethod;
  standardCost: number;
  fifoCost?: number;
  averageCost: number;
  recommendedCost: number;
}

// ============================================================================
// SERVICE
// ============================================================================

class ValuationService {
  private valuationLayerRepository: Repository<StockValuationLayer>;
  private productRepository: Repository<Product>;
  private stockMoveRepository: Repository<StockMove>;
  private locationRepository: Repository<Location>;
  private pickingRepository: Repository<Picking>;

  constructor() {
    this.valuationLayerRepository = AppDataSource.getRepository(StockValuationLayer);
    this.productRepository = AppDataSource.getRepository(Product);
    this.stockMoveRepository = AppDataSource.getRepository(StockMove);
    this.locationRepository = AppDataSource.getRepository(Location);
    this.pickingRepository = AppDataSource.getRepository(Picking);
  }

  /**
   * Create a new valuation layer (for incoming stock)
   * Used when receiving products via purchase orders or inventory adjustments
   */
  async createLayer(
    dto: CreateValuationLayerDto,
    tenantId: string,
    userId: string,
    queryRunner?: QueryRunner
  ): Promise<StockValuationLayer> {
    try {
      const value = dto.quantity * dto.unitCost;

      // Create layer entity
      const layerData = {
        tenantId,
        productId: dto.productId,
        companyId: dto.companyId,
        quantity: dto.quantity,
        unitCost: dto.unitCost,
        value,
        remainingQty: dto.quantity,
        remainingValue: value,
        stockMoveId: dto.stockMoveId || null,
        description: dto.description || null,
        createdBy: userId,
      };

      let layer: StockValuationLayer;

      if (queryRunner) {
        // Use queryRunner transaction
        const layerEntity = queryRunner.manager.create(StockValuationLayer, layerData);
        layer = await queryRunner.manager.save(StockValuationLayer, layerEntity);
      } else {
        // Use regular repository
        const layerEntity = this.valuationLayerRepository.create(layerData);
        layer = await this.valuationLayerRepository.save(layerEntity);
      }

      logger.info('Valuation layer created', {
        layerId: layer.id,
        productId: dto.productId,
        quantity: dto.quantity,
        unitCost: dto.unitCost,
      });

      return layer;
    } catch (error) {
      logger.error('Error creating valuation layer', {
        error: (error as Error).message,
        dto,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Consume stock using FIFO method
   * Returns the layers consumed and total cost
   */
  async consumeFifo(
    productId: string,
    companyId: string,
    quantity: number,
    tenantId: string,
    userId: string,
    queryRunner?: QueryRunner
  ): Promise<FifoConsumptionResult> {
    const shouldManageTransaction = !queryRunner;
    const runner = queryRunner || AppDataSource.createQueryRunner();

    if (shouldManageTransaction) {
      await runner.connect();
      await runner.startTransaction();
    }

    try {
      // Get available layers ordered by creation date (FIFO) with row lock
      const layers = await runner.manager
        .createQueryBuilder(StockValuationLayer, 'svl')
        .where('svl.productId = :productId', { productId })
        .andWhere('svl.companyId = :companyId', { companyId })
        .andWhere('svl.tenantId = :tenantId', { tenantId })
        .andWhere('svl.remainingQty > 0')
        .orderBy('svl.createdAt', 'ASC')
        .setLock('pessimistic_write')
        .getMany();

      let remainingToConsume = quantity;
      const consumedLayers: FifoConsumptionResult['layersConsumed'] = [];
      let totalCost = 0;

      for (const layer of layers) {
        if (remainingToConsume <= 0) break;

        const consumeFromLayer = Math.min(remainingToConsume, Number(layer.remainingQty));
        const valueConsumed = consumeFromLayer * Number(layer.unitCost);

        // Update layer
        await runner.manager
          .createQueryBuilder()
          .update(StockValuationLayer)
          .set({
            remainingQty: Number(layer.remainingQty) - consumeFromLayer,
            remainingValue: Number(layer.remainingValue) - valueConsumed,
            updatedAt: new Date(),
            updatedBy: userId,
          })
          .where('id = :id', { id: layer.id })
          .execute();

        consumedLayers.push({
          layerId: layer.id,
          quantityConsumed: consumeFromLayer,
          unitCost: Number(layer.unitCost),
          valueConsumed: valueConsumed,
        });

        totalCost += valueConsumed;
        remainingToConsume -= consumeFromLayer;
      }

      if (remainingToConsume > 0) {
        // Not enough stock in layers - this is a warning, not an error
        // The stock might exist without valuation layers (e.g., initial data)
        logger.warn('Insufficient valuation layers for FIFO consumption', {
          productId,
          requestedQty: quantity,
          availableQty: quantity - remainingToConsume,
        });
      }

      if (shouldManageTransaction) {
        await runner.commitTransaction();
      }

      const weightedAvgCost = quantity > 0 ? totalCost / (quantity - remainingToConsume) : 0;

      return {
        layersConsumed: consumedLayers,
        totalCost,
        weightedAverageCost: weightedAvgCost,
      };
    } catch (error) {
      if (shouldManageTransaction) {
        await runner.rollbackTransaction();
      }
      logger.error('Error consuming FIFO layers', {
        error: (error as Error).message,
        productId,
        companyId,
        quantity,
        tenantId,
      });
      throw error;
    } finally {
      if (shouldManageTransaction) {
        await runner.release();
      }
    }
  }

  /**
   * Calculate the current cost of a product based on its valuation method
   */
  async getProductCost(
    productId: string,
    companyId: string,
    tenantId: string
  ): Promise<ProductCostResult> {
    try {
      // Get product with its valuation method and standard cost
      const product = await this.productRepository
        .createQueryBuilder('product')
        .select(['product.id', 'product.valuationMethod', 'product.costPrice'])
        .where('product.id = :productId', { productId })
        .andWhere('product.tenantId = :tenantId', { tenantId })
        .getOne();

      if (!product) {
        throw new NotFoundError('Producto no encontrado');
      }

      // Get FIFO cost (oldest layer's unit cost)
      const oldestLayer = await this.valuationLayerRepository
        .createQueryBuilder('svl')
        .select('svl.unitCost')
        .where('svl.productId = :productId', { productId })
        .andWhere('svl.companyId = :companyId', { companyId })
        .andWhere('svl.tenantId = :tenantId', { tenantId })
        .andWhere('svl.remainingQty > 0')
        .orderBy('svl.createdAt', 'ASC')
        .limit(1)
        .getOne();

      // Get average cost from all layers
      const avgResult = await this.valuationLayerRepository
        .createQueryBuilder('svl')
        .select('SUM(svl.remainingValue)', 'totalValue')
        .addSelect('SUM(svl.remainingQty)', 'totalQty')
        .where('svl.productId = :productId', { productId })
        .andWhere('svl.companyId = :companyId', { companyId })
        .andWhere('svl.tenantId = :tenantId', { tenantId })
        .andWhere('svl.remainingQty > 0')
        .getRawOne();

      const standardCost = Number(product.costPrice) || 0;
      const fifoCost = oldestLayer ? Number(oldestLayer.unitCost) : undefined;

      const totalQty = parseFloat(avgResult?.totalQty) || 0;
      const totalValue = parseFloat(avgResult?.totalValue) || 0;
      const averageCost = totalQty > 0 ? totalValue / totalQty : 0;

      // Determine recommended cost based on valuation method
      let recommendedCost: number;
      switch (product.valuationMethod) {
        case ValuationMethod.FIFO:
          recommendedCost = fifoCost ?? standardCost;
          break;
        case ValuationMethod.AVERAGE:
          recommendedCost = averageCost > 0 ? averageCost : standardCost;
          break;
        case ValuationMethod.STANDARD:
        default:
          recommendedCost = standardCost;
          break;
      }

      return {
        productId,
        valuationMethod: product.valuationMethod,
        standardCost,
        fifoCost,
        averageCost,
        recommendedCost,
      };
    } catch (error) {
      logger.error('Error getting product cost', {
        error: (error as Error).message,
        productId,
        companyId,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Get valuation summary for a product
   */
  async getProductValuationSummary(
    productId: string,
    companyId: string,
    tenantId: string
  ): Promise<ValuationSummary | null> {
    try {
      const result = await this.productRepository
        .createQueryBuilder('p')
        .select('p.id', 'productId')
        .addSelect('p.name', 'productName')
        .addSelect('p.code', 'productCode')
        .addSelect('p.valuationMethod', 'valuationMethod')
        .addSelect('COALESCE(SUM(svl.remainingQty), 0)', 'totalQuantity')
        .addSelect('COALESCE(SUM(svl.remainingValue), 0)', 'totalValue')
        .addSelect(
          `CASE WHEN COALESCE(SUM(svl.remainingQty), 0) > 0
                THEN COALESCE(SUM(svl.remainingValue), 0) / SUM(svl.remainingQty)
                ELSE p.costPrice
           END`,
          'averageCost'
        )
        .addSelect('COUNT(CASE WHEN svl.remainingQty > 0 THEN 1 END)', 'layerCount')
        .leftJoin(
          StockValuationLayer,
          'svl',
          'p.id = svl.productId AND svl.companyId = :companyId AND svl.tenantId = :tenantId',
          { companyId, tenantId }
        )
        .where('p.id = :productId', { productId })
        .andWhere('p.tenantId = :tenantId', { tenantId })
        .groupBy('p.id')
        .addGroupBy('p.name')
        .addGroupBy('p.code')
        .addGroupBy('p.valuationMethod')
        .addGroupBy('p.costPrice')
        .getRawOne();

      if (!result) {
        return null;
      }

      return {
        productId: result.productId,
        productName: result.productName,
        productCode: result.productCode,
        totalQuantity: parseFloat(result.totalQuantity) || 0,
        totalValue: parseFloat(result.totalValue) || 0,
        averageCost: parseFloat(result.averageCost) || 0,
        valuationMethod: result.valuationMethod,
        layerCount: parseInt(result.layerCount) || 0,
      };
    } catch (error) {
      logger.error('Error getting product valuation summary', {
        error: (error as Error).message,
        productId,
        companyId,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Get all valuation layers for a product
   */
  async getProductLayers(
    productId: string,
    companyId: string,
    tenantId: string,
    includeEmpty: boolean = false
  ): Promise<StockValuationLayer[]> {
    try {
      const queryBuilder = this.valuationLayerRepository
        .createQueryBuilder('svl')
        .where('svl.productId = :productId', { productId })
        .andWhere('svl.companyId = :companyId', { companyId })
        .andWhere('svl.tenantId = :tenantId', { tenantId });

      if (!includeEmpty) {
        queryBuilder.andWhere('svl.remainingQty > 0');
      }

      const layers = await queryBuilder.orderBy('svl.createdAt', 'ASC').getMany();

      return layers;
    } catch (error) {
      logger.error('Error getting product layers', {
        error: (error as Error).message,
        productId,
        companyId,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Get inventory valuation report for a company
   */
  async getCompanyValuationReport(
    companyId: string,
    tenantId: string
  ): Promise<ValuationSummary[]> {
    try {
      const results = await this.productRepository
        .createQueryBuilder('p')
        .select('p.id', 'productId')
        .addSelect('p.name', 'productName')
        .addSelect('p.code', 'productCode')
        .addSelect('p.valuationMethod', 'valuationMethod')
        .addSelect('COALESCE(SUM(svl.remainingQty), 0)', 'totalQuantity')
        .addSelect('COALESCE(SUM(svl.remainingValue), 0)', 'totalValue')
        .addSelect(
          `CASE WHEN COALESCE(SUM(svl.remainingQty), 0) > 0
                THEN COALESCE(SUM(svl.remainingValue), 0) / SUM(svl.remainingQty)
                ELSE p.costPrice
           END`,
          'averageCost'
        )
        .addSelect('COUNT(CASE WHEN svl.remainingQty > 0 THEN 1 END)', 'layerCount')
        .leftJoin(
          StockValuationLayer,
          'svl',
          'p.id = svl.productId AND svl.companyId = :companyId AND svl.tenantId = :tenantId',
          { companyId, tenantId }
        )
        .where('p.tenantId = :tenantId', { tenantId })
        .andWhere('p.productType = :productType', { productType: 'storable' })
        .andWhere('p.active = :active', { active: true })
        .groupBy('p.id')
        .addGroupBy('p.name')
        .addGroupBy('p.code')
        .addGroupBy('p.valuationMethod')
        .addGroupBy('p.costPrice')
        .having('COALESCE(SUM(svl.remainingQty), 0) > 0')
        .orderBy('p.name', 'ASC')
        .getRawMany();

      return results.map((row) => ({
        productId: row.productId,
        productName: row.productName,
        productCode: row.productCode,
        totalQuantity: parseFloat(row.totalQuantity) || 0,
        totalValue: parseFloat(row.totalValue) || 0,
        averageCost: parseFloat(row.averageCost) || 0,
        valuationMethod: row.valuationMethod,
        layerCount: parseInt(row.layerCount) || 0,
      }));
    } catch (error) {
      logger.error('Error getting company valuation report', {
        error: (error as Error).message,
        companyId,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Update average cost on product after valuation changes
   * Call this after creating layers or consuming stock
   */
  async updateProductAverageCost(
    productId: string,
    companyId: string,
    tenantId: string,
    queryRunner?: QueryRunner
  ): Promise<void> {
    try {
      // Calculate new average cost
      const repo = queryRunner?.manager.getRepository(StockValuationLayer) || this.valuationLayerRepository;
      const avgResult = await repo
        .createQueryBuilder('svl')
        .select('SUM(svl.remainingValue)', 'totalValue')
        .addSelect('SUM(svl.remainingQty)', 'totalQty')
        .where('svl.productId = :productId', { productId })
        .andWhere('svl.companyId = :companyId', { companyId })
        .andWhere('svl.tenantId = :tenantId', { tenantId })
        .andWhere('svl.remainingQty > 0')
        .getRawOne();

      const totalQty = parseFloat(avgResult?.totalQty) || 0;
      const totalValue = parseFloat(avgResult?.totalValue) || 0;

      if (totalQty > 0) {
        const newAverageCost = totalValue / totalQty;

        // Only update products using average cost method
        const updateQuery = (queryRunner?.manager || this.productRepository)
          .createQueryBuilder()
          .update(Product)
          .set({
            costPrice: newAverageCost,
            updatedAt: new Date(),
          })
          .where('id = :productId', { productId })
          .andWhere('tenantId = :tenantId', { tenantId })
          .andWhere('valuationMethod = :method', { method: ValuationMethod.AVERAGE });

        await updateQuery.execute();
      }
    } catch (error) {
      logger.error('Error updating product average cost', {
        error: (error as Error).message,
        productId,
        companyId,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Process stock move for valuation
   * Creates or consumes valuation layers based on move direction
   */
  async processStockMoveValuation(
    moveId: string,
    tenantId: string,
    userId: string
  ): Promise<void> {
    const queryRunner = AppDataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      // Get stock move with related data
      const move = await queryRunner.manager
        .createQueryBuilder(StockMove, 'sm')
        .select([
          'sm.id',
          'sm.productId',
          'sm.productQty',
          'sm.locationId',
          'sm.locationDestId',
        ])
        .addSelect('p.companyId')
        .innerJoin(Picking, 'p', 'sm.pickingId = p.id')
        .where('sm.id = :moveId', { moveId })
        .andWhere('sm.tenantId = :tenantId', { tenantId })
        .getRawOne();

      if (!move) {
        throw new NotFoundError('Movimiento no encontrado');
      }

      // Get location types
      const [srcLoc, destLoc] = await Promise.all([
        queryRunner.manager.findOne(Location, {
          where: { id: move.sm_location_id },
          select: ['locationType'],
        }),
        queryRunner.manager.findOne(Location, {
          where: { id: move.sm_location_dest_id },
          select: ['locationType'],
        }),
      ]);

      const srcIsInternal = srcLoc?.locationType === 'internal';
      const destIsInternal = destLoc?.locationType === 'internal';

      // Get product cost and valuation method
      const product = await queryRunner.manager.findOne(Product, {
        where: { id: move.sm_product_id },
        select: ['costPrice', 'valuationMethod'],
      });

      if (!product) {
        throw new NotFoundError('Producto no encontrado');
      }

      // Incoming to internal location (create layer)
      if (!srcIsInternal && destIsInternal) {
        await this.createLayer(
          {
            productId: move.sm_product_id,
            companyId: move.p_company_id,
            quantity: Number(move.sm_product_qty),
            unitCost: Number(product.costPrice),
            stockMoveId: move.sm_id,
            description: `Recepción - Move ${move.sm_id}`,
          },
          tenantId,
          userId,
          queryRunner
        );
      }

      // Outgoing from internal location (consume layer with FIFO)
      if (srcIsInternal && !destIsInternal) {
        if (
          product.valuationMethod === ValuationMethod.FIFO ||
          product.valuationMethod === ValuationMethod.AVERAGE
        ) {
          await this.consumeFifo(
            move.sm_product_id,
            move.p_company_id,
            Number(move.sm_product_qty),
            tenantId,
            userId,
            queryRunner
          );
        }
      }

      // Update average cost if using that method
      if (product.valuationMethod === ValuationMethod.AVERAGE) {
        await this.updateProductAverageCost(
          move.sm_product_id,
          move.p_company_id,
          tenantId,
          queryRunner
        );
      }

      await queryRunner.commitTransaction();

      logger.info('Stock move valuation processed', {
        moveId,
        productId: move.sm_product_id,
        quantity: move.sm_product_qty,
        srcIsInternal,
        destIsInternal,
        valuationMethod: product.valuationMethod,
      });
    } catch (error) {
      await queryRunner.rollbackTransaction();
      logger.error('Error processing stock move valuation', {
        error: (error as Error).message,
        moveId,
        tenantId,
      });
      throw error;
    } finally {
      await queryRunner.release();
    }
  }
}

// ============================================================================
// EXPORT
// ============================================================================

export const valuationService = new ValuationService();

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