import { Repository, DataSource } from 'typeorm';
import {
  PurchaseOrder,
  PurchaseOrderItem,
  PurchaseReceipt,
  PurchaseReceiptItem,
  PurchaseOrderMatching,
  PurchaseMatchingLine,
  MatchingException,
  MatchingStatus,
  MatchingLineStatus,
  ExceptionType,
} from '../entities';
import {
  MatchingStatusDto,
  MatchingLineStatusDto,
  MatchingExceptionDto,
  MatchingSummaryDto,
  ResolveExceptionDto,
} from '../dto';

/**
 * Tolerance configuration for matching validation
 */
const TOLERANCES = {
  QUANTITY_PERCENT: 0.5, // 0.5% tolerance for quantity variance
  PRICE_PERCENT: 2.0, // 2% tolerance for price variance
};

export interface MatchingSearchParams {
  tenantId: string;
  status?: MatchingStatus;
  supplierId?: string;
  hasExceptions?: boolean;
  limit?: number;
  offset?: number;
}

/**
 * Interface for invoice data (simplified for matching purposes)
 */
export interface InvoiceLineData {
  orderItemId: string;
  quantityInvoiced: number;
  priceInvoiced: number;
}

export interface InvoiceData {
  invoiceId: string;
  purchaseOrderId: string;
  totalInvoiced: number;
  lines: InvoiceLineData[];
}

export class ThreeWayMatchingService {
  constructor(
    private readonly matchingRepository: Repository<PurchaseOrderMatching>,
    private readonly matchingLineRepository: Repository<PurchaseMatchingLine>,
    private readonly exceptionRepository: Repository<MatchingException>,
    private readonly orderRepository: Repository<PurchaseOrder>,
    private readonly orderItemRepository: Repository<PurchaseOrderItem>,
    private readonly receiptRepository: Repository<PurchaseReceipt>,
    private readonly receiptItemRepository: Repository<PurchaseReceiptItem>,
    private readonly dataSource: DataSource
  ) {}

  // ==================== Initialization ====================

  /**
   * Initialize matching record when a purchase order is confirmed
   * Called when PO status changes to 'confirmed'
   */
  async initializeMatching(purchaseOrderId: string, tenantId: string): Promise<PurchaseOrderMatching> {
    // Check if matching already exists
    const existing = await this.matchingRepository.findOne({
      where: { purchaseOrderId, tenantId },
    });

    if (existing) {
      return existing;
    }

    // Get order with items
    const order = await this.orderRepository.findOne({
      where: { id: purchaseOrderId, tenantId },
    });

    if (!order) {
      throw new Error(`Purchase order ${purchaseOrderId} not found`);
    }

    const orderItems = await this.orderItemRepository.find({
      where: { orderId: purchaseOrderId },
    });

    if (orderItems.length === 0) {
      throw new Error(`Purchase order ${purchaseOrderId} has no items`);
    }

    // Create matching record within transaction
    return this.dataSource.transaction(async (manager) => {
      const matchingRepo = manager.getRepository(PurchaseOrderMatching);
      const matchingLineRepo = manager.getRepository(PurchaseMatchingLine);

      const matching = matchingRepo.create({
        tenantId,
        purchaseOrderId,
        status: 'pending',
        totalOrdered: Number(order.total),
        totalReceived: 0,
        totalInvoiced: 0,
      });

      const savedMatching = await matchingRepo.save(matching);

      // Create matching lines for each order item
      const lines = orderItems.map((item) =>
        matchingLineRepo.create({
          matchingId: savedMatching.id,
          orderItemId: item.id,
          tenantId,
          qtyOrdered: Number(item.quantity),
          qtyReceived: 0,
          qtyInvoiced: 0,
          priceOrdered: Number(item.unitPrice),
          priceInvoiced: 0,
          status: 'pending',
        })
      );

      await matchingLineRepo.save(lines);

      return savedMatching;
    });
  }

  // ==================== Receipt Updates ====================

  /**
   * Update matching quantities from a confirmed receipt
   */
  async updateFromReceipt(receiptId: string, tenantId: string): Promise<PurchaseOrderMatching> {
    const receipt = await this.receiptRepository.findOne({
      where: { id: receiptId, tenantId },
    });

    if (!receipt) {
      throw new Error(`Receipt ${receiptId} not found`);
    }

    const receiptItems = await this.receiptItemRepository.find({
      where: { receiptId },
    });

    // Get or create matching record
    let matching = await this.matchingRepository.findOne({
      where: { purchaseOrderId: receipt.orderId, tenantId },
    });

    if (!matching) {
      matching = await this.initializeMatching(receipt.orderId, tenantId);
    }

    return this.dataSource.transaction(async (manager) => {
      const matchingRepo = manager.getRepository(PurchaseOrderMatching);
      const matchingLineRepo = manager.getRepository(PurchaseMatchingLine);
      const exceptionRepo = manager.getRepository(MatchingException);

      // Update matching lines from receipt items
      for (const receiptItem of receiptItems) {
        if (!receiptItem.orderItemId) continue;

        const matchingLine = await matchingLineRepo.findOne({
          where: { matchingId: matching!.id, orderItemId: receiptItem.orderItemId },
        });

        if (matchingLine) {
          // Accumulate received quantity
          matchingLine.qtyReceived = Number(matchingLine.qtyReceived) + Number(receiptItem.quantityReceived);
          await matchingLineRepo.save(matchingLine);

          // Check for receipt quantity exceptions
          await this.checkReceiptQuantityException(
            matchingLine,
            exceptionRepo,
            tenantId
          );
        }
      }

      // Recalculate totals
      const lines = await matchingLineRepo.find({
        where: { matchingId: matching!.id },
      });

      const orderItems = await this.orderItemRepository.find({
        where: { orderId: matching!.purchaseOrderId },
      });

      // Create a map for quick lookup
      const orderItemMap = new Map(orderItems.map((item) => [item.id, item]));

      let totalReceived = 0;
      for (const line of lines) {
        const orderItem = orderItemMap.get(line.orderItemId);
        if (orderItem) {
          totalReceived += Number(line.qtyReceived) * Number(orderItem.unitPrice);
        }
      }

      matching!.totalReceived = totalReceived;
      matching!.lastReceiptId = receiptId;

      // Update status
      matching!.status = this.calculateMatchingStatus(matching!, lines);

      return matchingRepo.save(matching!);
    });
  }

  // ==================== Invoice Updates ====================

  /**
   * Update matching from an invoice
   */
  async updateFromInvoice(invoiceData: InvoiceData, tenantId: string): Promise<PurchaseOrderMatching> {
    // Get or create matching record
    let matching = await this.matchingRepository.findOne({
      where: { purchaseOrderId: invoiceData.purchaseOrderId, tenantId },
    });

    if (!matching) {
      matching = await this.initializeMatching(invoiceData.purchaseOrderId, tenantId);
    }

    return this.dataSource.transaction(async (manager) => {
      const matchingRepo = manager.getRepository(PurchaseOrderMatching);
      const matchingLineRepo = manager.getRepository(PurchaseMatchingLine);
      const exceptionRepo = manager.getRepository(MatchingException);

      // Update matching lines from invoice lines
      for (const invoiceLine of invoiceData.lines) {
        const matchingLine = await matchingLineRepo.findOne({
          where: { matchingId: matching!.id, orderItemId: invoiceLine.orderItemId },
        });

        if (matchingLine) {
          // Accumulate invoiced quantity and update price
          matchingLine.qtyInvoiced = Number(matchingLine.qtyInvoiced) + invoiceLine.quantityInvoiced;
          matchingLine.priceInvoiced = invoiceLine.priceInvoiced;
          await matchingLineRepo.save(matchingLine);

          // Check for invoice exceptions
          await this.checkInvoiceExceptions(
            matchingLine,
            exceptionRepo,
            tenantId
          );
        }
      }

      // Update totals
      matching!.totalInvoiced = Number(matching!.totalInvoiced) + invoiceData.totalInvoiced;
      matching!.lastInvoiceId = invoiceData.invoiceId;

      // Recalculate status
      const lines = await matchingLineRepo.find({
        where: { matchingId: matching!.id },
      });
      matching!.status = this.calculateMatchingStatus(matching!, lines);

      return matchingRepo.save(matching!);
    });
  }

  // ==================== Matching Validation ====================

  /**
   * Perform complete matching validation for an order
   */
  async performMatching(matchingId: string, tenantId: string, userId?: string): Promise<PurchaseOrderMatching> {
    const matching = await this.matchingRepository.findOne({
      where: { id: matchingId, tenantId },
    });

    if (!matching) {
      throw new Error(`Matching record ${matchingId} not found`);
    }

    return this.dataSource.transaction(async (manager) => {
      const matchingRepo = manager.getRepository(PurchaseOrderMatching);
      const matchingLineRepo = manager.getRepository(PurchaseMatchingLine);
      const exceptionRepo = manager.getRepository(MatchingException);

      const lines = await matchingLineRepo.find({
        where: { matchingId },
      });

      // Validate each line
      for (const line of lines) {
        // Check receipt quantity variance
        await this.checkReceiptQuantityException(line, exceptionRepo, tenantId);

        // Check invoice quantity variance
        await this.checkInvoiceExceptions(line, exceptionRepo, tenantId);

        // Update line status
        line.status = this.calculateLineStatus(line);
        await matchingLineRepo.save(line);
      }

      // Check if all lines are matched and no pending exceptions
      const pendingExceptions = await exceptionRepo.count({
        where: { matchingId, status: 'pending' },
      });

      const allLinesMatched = lines.every((line) => line.status === 'matched');

      if (allLinesMatched && pendingExceptions === 0) {
        matching.status = 'matched';
        matching.matchedAt = new Date();
        matching.matchedBy = userId;
      } else if (lines.some((line) => line.status === 'mismatch') || pendingExceptions > 0) {
        matching.status = 'mismatch';
      }

      return matchingRepo.save(matching);
    });
  }

  // ==================== Status Queries ====================

  /**
   * Get complete matching status for a purchase order
   */
  async getMatchingStatus(purchaseOrderId: string, tenantId: string): Promise<MatchingStatusDto | null> {
    const matching = await this.matchingRepository.findOne({
      where: { purchaseOrderId, tenantId },
      relations: ['purchaseOrder'],
    });

    if (!matching) {
      return null;
    }

    const lines = await this.matchingLineRepository.find({
      where: { matchingId: matching.id },
    });

    const orderItems = await this.orderItemRepository.find({
      where: { orderId: purchaseOrderId },
    });

    const orderItemMap = new Map(orderItems.map((item) => [item.id, item]));

    const exceptions = await this.exceptionRepository.find({
      where: { matchingId: matching.id },
      order: { createdAt: 'DESC' },
    });

    const pendingExceptionsCount = exceptions.filter((e) => e.status === 'pending').length;

    const lineStatuses: MatchingLineStatusDto[] = lines.map((line) => {
      const orderItem = orderItemMap.get(line.orderItemId);
      return {
        id: line.id,
        orderItemId: line.orderItemId,
        productName: orderItem?.productName || 'Unknown',
        qtyOrdered: Number(line.qtyOrdered),
        qtyReceived: Number(line.qtyReceived),
        qtyInvoiced: Number(line.qtyInvoiced),
        priceOrdered: Number(line.priceOrdered),
        priceInvoiced: Number(line.priceInvoiced),
        qtyVariance: Number(line.qtyVariance) || 0,
        invoiceQtyVariance: Number(line.invoiceQtyVariance) || 0,
        priceVariance: Number(line.priceVariance) || 0,
        status: line.status,
      };
    });

    const exceptionDtos: MatchingExceptionDto[] = exceptions.map((exc) => ({
      id: exc.id,
      exceptionType: exc.exceptionType,
      expectedValue: exc.expectedValue ? Number(exc.expectedValue) : undefined,
      actualValue: exc.actualValue ? Number(exc.actualValue) : undefined,
      varianceValue: exc.varianceValue ? Number(exc.varianceValue) : undefined,
      variancePercent: exc.variancePercent ? Number(exc.variancePercent) : undefined,
      status: exc.status,
      resolvedAt: exc.resolvedAt?.toISOString(),
      resolutionNotes: exc.resolutionNotes,
    }));

    return {
      id: matching.id,
      purchaseOrderId: matching.purchaseOrderId,
      orderNumber: matching.purchaseOrder?.orderNumber || '',
      status: matching.status,
      totalOrdered: Number(matching.totalOrdered),
      totalReceived: Number(matching.totalReceived),
      totalInvoiced: Number(matching.totalInvoiced),
      receiptVariance: Number(matching.receiptVariance) || 0,
      invoiceVariance: Number(matching.invoiceVariance) || 0,
      lastReceiptId: matching.lastReceiptId,
      lastInvoiceId: matching.lastInvoiceId,
      matchedAt: matching.matchedAt?.toISOString(),
      lines: lineStatuses,
      exceptions: exceptionDtos,
      pendingExceptionsCount,
    };
  }

  /**
   * Get list of matchings with summary
   */
  async getMatchingList(params: MatchingSearchParams): Promise<{ data: MatchingSummaryDto[]; total: number }> {
    const { tenantId, status, supplierId, hasExceptions, limit = 50, offset = 0 } = params;

    const qb = this.matchingRepository
      .createQueryBuilder('matching')
      .leftJoinAndSelect('matching.purchaseOrder', 'po')
      .where('matching.tenant_id = :tenantId', { tenantId });

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

    if (supplierId) {
      qb.andWhere('po.supplier_id = :supplierId', { supplierId });
    }

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

    // Get exception counts
    const matchingIds = matchings.map((m) => m.id);
    let exceptionCounts: Map<string, number> = new Map();

    if (matchingIds.length > 0) {
      const countResults = await this.exceptionRepository
        .createQueryBuilder('exc')
        .select('exc.matching_id', 'matchingId')
        .addSelect('COUNT(*)', 'count')
        .where('exc.matching_id IN (:...matchingIds)', { matchingIds })
        .andWhere('exc.status = :status', { status: 'pending' })
        .groupBy('exc.matching_id')
        .getRawMany();

      exceptionCounts = new Map(countResults.map((r) => [r.matchingId, parseInt(r.count)]));
    }

    const data: MatchingSummaryDto[] = matchings
      .filter((matching) => {
        if (hasExceptions === true) {
          return (exceptionCounts.get(matching.id) || 0) > 0;
        }
        if (hasExceptions === false) {
          return (exceptionCounts.get(matching.id) || 0) === 0;
        }
        return true;
      })
      .map((matching) => ({
        id: matching.id,
        purchaseOrderId: matching.purchaseOrderId,
        orderNumber: matching.purchaseOrder?.orderNumber || '',
        supplierName: matching.purchaseOrder?.supplierName || '',
        status: matching.status,
        totalOrdered: Number(matching.totalOrdered),
        totalReceived: Number(matching.totalReceived),
        totalInvoiced: Number(matching.totalInvoiced),
        pendingExceptionsCount: exceptionCounts.get(matching.id) || 0,
        createdAt: matching.createdAt.toISOString(),
        matchedAt: matching.matchedAt?.toISOString(),
      }));

    return { data, total };
  }

  // ==================== Exception Management ====================

  /**
   * Resolve an exception
   */
  async resolveException(
    exceptionId: string,
    tenantId: string,
    dto: ResolveExceptionDto,
    userId: string
  ): Promise<MatchingException> {
    const exception = await this.exceptionRepository.findOne({
      where: { id: exceptionId, tenantId },
    });

    if (!exception) {
      throw new Error(`Exception ${exceptionId} not found`);
    }

    if (exception.status !== 'pending') {
      throw new Error(`Exception ${exceptionId} is already resolved`);
    }

    exception.status = dto.resolution;
    exception.resolvedAt = new Date();
    exception.resolvedBy = userId;
    exception.resolutionNotes = dto.notes;

    const saved = await this.exceptionRepository.save(exception);

    // Re-evaluate matching status after resolving exception
    if (exception.matchingId) {
      await this.performMatching(exception.matchingId, tenantId, userId);
    }

    return saved;
  }

  /**
   * Get pending exceptions for a tenant
   */
  async getPendingExceptions(
    tenantId: string,
    limit = 50,
    offset = 0
  ): Promise<{ data: MatchingException[]; total: number }> {
    const [data, total] = await this.exceptionRepository.findAndCount({
      where: { tenantId, status: 'pending' },
      order: { createdAt: 'DESC' },
      take: limit,
      skip: offset,
    });

    return { data, total };
  }

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

  /**
   * Check and create receipt quantity exceptions
   */
  private async checkReceiptQuantityException(
    line: PurchaseMatchingLine,
    exceptionRepo: Repository<MatchingException>,
    tenantId: string
  ): Promise<void> {
    const qtyOrdered = Number(line.qtyOrdered);
    const qtyReceived = Number(line.qtyReceived);
    const variance = qtyOrdered - qtyReceived;
    const variancePercent = qtyOrdered > 0 ? Math.abs(variance / qtyOrdered) * 100 : 0;

    // Check if variance exceeds tolerance
    if (variancePercent > TOLERANCES.QUANTITY_PERCENT) {
      const exceptionType: ExceptionType = variance > 0 ? 'short_receipt' : 'over_receipt';

      // Check if exception already exists
      const existing = await exceptionRepo.findOne({
        where: {
          matchingLineId: line.id,
          exceptionType,
          status: 'pending',
        },
      });

      if (!existing) {
        const exception = exceptionRepo.create({
          tenantId,
          matchingId: line.matchingId,
          matchingLineId: line.id,
          exceptionType,
          expectedValue: qtyOrdered,
          actualValue: qtyReceived,
          varianceValue: Math.abs(variance),
          variancePercent,
          status: 'pending',
        });

        await exceptionRepo.save(exception);
      }
    }
  }

  /**
   * Check and create invoice exceptions (quantity and price)
   */
  private async checkInvoiceExceptions(
    line: PurchaseMatchingLine,
    exceptionRepo: Repository<MatchingException>,
    tenantId: string
  ): Promise<void> {
    const qtyReceived = Number(line.qtyReceived);
    const qtyInvoiced = Number(line.qtyInvoiced);
    const priceOrdered = Number(line.priceOrdered);
    const priceInvoiced = Number(line.priceInvoiced);

    // Check invoice quantity variance (invoice vs receipt)
    if (qtyInvoiced > qtyReceived) {
      const variance = qtyInvoiced - qtyReceived;
      const variancePercent = qtyReceived > 0 ? (variance / qtyReceived) * 100 : 100;

      if (variancePercent > TOLERANCES.QUANTITY_PERCENT) {
        const existing = await exceptionRepo.findOne({
          where: {
            matchingLineId: line.id,
            exceptionType: 'over_invoice',
            status: 'pending',
          },
        });

        if (!existing) {
          const exception = exceptionRepo.create({
            tenantId,
            matchingId: line.matchingId,
            matchingLineId: line.id,
            exceptionType: 'over_invoice',
            expectedValue: qtyReceived,
            actualValue: qtyInvoiced,
            varianceValue: variance,
            variancePercent,
            status: 'pending',
          });

          await exceptionRepo.save(exception);
        }
      }
    }

    // Check price variance
    if (priceInvoiced > 0 && priceOrdered > 0) {
      const priceVarianceAmount = priceInvoiced - priceOrdered;
      const priceVariancePercent = (priceVarianceAmount / priceOrdered) * 100;

      // Only create exception if invoiced price exceeds ordered price by more than tolerance
      if (priceVariancePercent > TOLERANCES.PRICE_PERCENT) {
        const existing = await exceptionRepo.findOne({
          where: {
            matchingLineId: line.id,
            exceptionType: 'price_variance',
            status: 'pending',
          },
        });

        if (!existing) {
          const exception = exceptionRepo.create({
            tenantId,
            matchingId: line.matchingId,
            matchingLineId: line.id,
            exceptionType: 'price_variance',
            expectedValue: priceOrdered,
            actualValue: priceInvoiced,
            varianceValue: priceVarianceAmount,
            variancePercent: priceVariancePercent,
            status: 'pending',
          });

          await exceptionRepo.save(exception);
        }
      }
    }
  }

  /**
   * Calculate matching status based on current state
   */
  private calculateMatchingStatus(
    matching: PurchaseOrderMatching,
    lines: PurchaseMatchingLine[]
  ): MatchingStatus {
    const totalOrdered = Number(matching.totalOrdered);
    const totalReceived = Number(matching.totalReceived);
    const totalInvoiced = Number(matching.totalInvoiced);

    // No receipts yet
    if (totalReceived === 0) {
      return 'pending';
    }

    // Partial receipt
    if (totalReceived < totalOrdered * 0.995) {
      return 'partial_receipt';
    }

    // Fully received, no invoice
    if (totalInvoiced === 0) {
      return 'received';
    }

    // Partial invoice
    if (totalInvoiced < totalReceived * 0.995) {
      return 'partial_invoice';
    }

    // Check all lines
    const allMatched = lines.every((line) => this.calculateLineStatus(line) === 'matched');

    return allMatched ? 'matched' : 'mismatch';
  }

  /**
   * Calculate line status based on quantities and prices
   */
  private calculateLineStatus(line: PurchaseMatchingLine): MatchingLineStatus {
    const qtyOrdered = Number(line.qtyOrdered);
    const qtyReceived = Number(line.qtyReceived);
    const qtyInvoiced = Number(line.qtyInvoiced);
    const priceOrdered = Number(line.priceOrdered);
    const priceInvoiced = Number(line.priceInvoiced);

    // No activity yet
    if (qtyReceived === 0 && qtyInvoiced === 0) {
      return 'pending';
    }

    // Check quantity tolerances
    const qtyReceivedVariance = Math.abs(qtyOrdered - qtyReceived) / qtyOrdered * 100;
    const qtyInvoicedVariance = qtyReceived > 0 ? Math.abs(qtyReceived - qtyInvoiced) / qtyReceived * 100 : 0;

    // Check price tolerance (only if invoiced)
    let priceVariance = 0;
    if (priceInvoiced > 0 && priceOrdered > 0) {
      priceVariance = Math.abs(priceInvoiced - priceOrdered) / priceOrdered * 100;
    }

    // Partial if not fully received or invoiced
    if (qtyReceived < qtyOrdered * 0.995 || qtyInvoiced < qtyReceived * 0.995) {
      return 'partial';
    }

    // Mismatch if any variance exceeds tolerance
    if (
      qtyReceivedVariance > TOLERANCES.QUANTITY_PERCENT ||
      qtyInvoicedVariance > TOLERANCES.QUANTITY_PERCENT ||
      priceVariance > TOLERANCES.PRICE_PERCENT
    ) {
      return 'mismatch';
    }

    return 'matched';
  }
}
