import { PurchaseRepository } from "../repositories/PurchaseRepository.js";
import { calculatePurchaseTotals } from "../utils/purchaseCalculations.js";
import prisma from "../config/prisma.js";
import { Prisma } from "../generated/prisma/client.js";

export class PurchaseService {
  constructor(private readonly purchaseRepository = new PurchaseRepository()) { }

  async getLotAvailability(lotNo: string) {
    const wholesaleAgg = await prisma.purchase.aggregate({
      _sum: { remainingQuantity: true, remainingBags: true },
      where: {
        lotNo,
        status: { not: "CLOSED" },
        deletedAt: null,
      },
    });

    const wholesaleRemaining = Number(wholesaleAgg._sum.remainingQuantity ?? 0);
    const wholesaleBagsRemaining = Number(wholesaleAgg._sum.remainingBags ?? 0);

    // Calculate retail remaining for this lot
    const retailStocks = await prisma.retailStock.aggregate({
      _sum: { remainingQuantity: true },
      where: { lotNo, deletedAt: null }
    });
    const retailRemaining = Number(retailStocks._sum.remainingQuantity ?? 0);

    const totalRemaining = wholesaleRemaining + retailRemaining;
    const isWholesaleDepleted = wholesaleRemaining <= 0 && wholesaleBagsRemaining <= 0;
    const isFullyDepleted = totalRemaining <= 0;

    return {
      wholesaleRemaining,
      wholesaleBagsRemaining,
      retailRemaining,
      totalRemaining,
      isWholesaleDepleted,
      isFullyDepleted,
    };
  }

  async checkLotAvailability(
    lotNo: string,
    vegetableId?: string | bigint
  ): Promise<{ isAvailable: boolean; message: string }> {
    // 1. Check for active purchase lots (unclosed flexible lot OR lot with remaining stock)
    const activeLotWhere: any = {
      lotNo,
      status: { not: "CLOSED" },
      deletedAt: null,
      OR: [
        { remainingQuantity: { gt: 0 } },
        { remainingBags: { gt: 0 } },
        { vegetable: { isFlexible: true } },
      ],
    };
    if (vegetableId) {
      activeLotWhere.vegetableId = BigInt(vegetableId);
    }

    const activeLot = await prisma.purchase.findFirst({
      where: activeLotWhere,
      include: {
        vegetable: { select: { id: true, name: true, isFlexible: true } },
      },
    });

    if (activeLot) {
      const isFlexible = activeLot.vegetable?.isFlexible;
      const remQty = Number(activeLot.remainingQuantity || 0);
      const remBags = Number(activeLot.remainingBags || 0);

      if (isFlexible) {
        return {
          isAvailable: false,
          message: `Lot ${lotNo} is currently active and must be closed before reuse.`,
        };
      }

      let stockInfo = "";
      if (remQty > 0 && remBags > 0) {
        stockInfo = ` (${remQty} KG / ${remBags} ${activeLot.unit || "BAG"} remaining)`;
      } else if (remBags > 0) {
        stockInfo = ` (${remBags} ${activeLot.unit || "BAG"} remaining)`;
      } else if (remQty > 0) {
        stockInfo = ` (${remQty} KG remaining)`;
      }

      return {
        isAvailable: false,
        message: `Lot ${lotNo} already has active stock${stockInfo} and cannot be reused yet.`,
      };
    }

    // 2. Check retail remaining stock for this lot
    const retailStockWhere: any = {
      lotNo,
      deletedAt: null,
    };
    if (vegetableId) {
      retailStockWhere.purchase = { vegetableId: BigInt(vegetableId) };
    }

    const retailStocks = await prisma.retailStock.aggregate({
      _sum: { remainingQuantity: true, remainingBags: true },
      where: retailStockWhere,
    });
    const retailRemaining = Number(retailStocks._sum.remainingQuantity ?? 0);
    const retailBagsRemaining = Number(retailStocks._sum.remainingBags ?? 0);
    if (retailRemaining > 0 || retailBagsRemaining > 0) {
      const stockInfo =
        retailRemaining > 0 && retailBagsRemaining > 0
          ? `${retailRemaining} KG / ${retailBagsRemaining} Bags`
          : retailRemaining > 0
            ? `${retailRemaining} KG`
            : `${retailBagsRemaining} Bags`;
      return {
        isAvailable: false,
        message: `Lot ${lotNo} still has active retail stock (${stockInfo} remaining).`,
      };
    }

    // --- WHY COMMENTED OUT ---
    // The business rule "Different vegetable + same lot + Out of Stock: Allow adding a different vegetable to the lot" 
    // means we can no longer permanently assign a lot number to just one vegetable. We must allow re-use.
    /*
    // 3. Vegetable conflict check
    if (vegetableId) {
      const previousUse = await this.purchaseRepository.findAnyByLotNo(lotNo);
      if (previousUse && previousUse.vegetableId !== BigInt(vegetableId)) {
        return {
          isAvailable: false,
          message: `Lot ${lotNo} is permanently assigned to a different vegetable.`,
        };
      }
    }
    */

    return {
      isAvailable: true,
      message: "Lot is available for use.",
    };
  }

  async isLotAvailableForNewPurchase(lotNo: string): Promise<boolean> {
    const result = await this.checkLotAvailability(lotNo);
    return result.isAvailable;
  }

  async createPurchase(data: any) {
    const availability = await this.checkLotAvailability(data.lotNo, data.vegetableId);
    if (!availability.isAvailable) {
      throw new Error(availability.message);
    }

    // --- WHY COMMENTED OUT ---
    // The business rule "Different vegetable + same lot + Out of Stock: Allow adding a different vegetable to the lot" 
    // means we can no longer permanently assign a lot number to just one vegetable. We must allow re-use.
    /*
    const previousUse = await this.purchaseRepository.findAnyByLotNo(data.lotNo);
    if (previousUse && previousUse.vegetableId !== BigInt(data.vegetableId)) {
      throw new Error(
        `Lot ${data.lotNo} is permanently assigned to a different vegetable and cannot be assigned to another.`
      );
    }
    */

    // Calculate totals
    const totals = calculatePurchaseTotals({
      unit: data.unit,
      rateType: data.rateType,
      quantity: data.quantity,
      bags: data.bags,
      ratePerUnit: data.ratePerUnit,
      transportFee: data.transportFee,
      loadingUnloadingFee: data.loadingUnloadingFee,
      otherCharges: data.otherCharges,
      thaiyalCharges: data.thaiyalCharges,
      extraCharges: data.extraCharges,
      advanceAmount: data.advanceAmount,
    });

    const { expenseAmount, expensePaymentMode, ...purchaseData } = data;

    const createDataPayload = {
      ...purchaseData,
      ...totals,
      quantity: totals.quantity,
      bags: totals.bags,
      remainingQuantity: totals.quantity,
      remainingBags: totals.bags,
      status: "ACTIVE" as const,
    };

    return await prisma.$transaction(async (tx) => {
      const purchase = await tx.purchase.create({ data: createDataPayload });

      // Increase supplier outstanding balance if supplier exists
      if (purchase.supplierId) {
        await tx.supplier.update({
          where: { id: purchase.supplierId },
          data: { outstandingBalance: { increment: purchase.balanceAmount } },
        });
      }

      // Handle extra purchase expense creating an Expense record
      if (data.expenseAmount && Number(data.expenseAmount) > 0 && data.expensePaymentMode) {
        await tx.expense.create({
          data: {
            expenseDate: data.purchaseDate ? new Date(data.purchaseDate) : new Date(),
            category: "Purchase Expense",
            amount: new Prisma.Decimal(data.expenseAmount),
            paymentMode: data.expensePaymentMode,
            purchaseId: purchase.id,
            remarks: `Purchase Order ID: ${purchase.id}, Invoice: ${purchase.billNo || 'N/A'}, Lot: ${purchase.lotNo}`,
          },
        });
      }

      return purchase;
    });
  }

  async bulkCreatePurchases(purchases: any[]): Promise<{
    created: any[];
    errors: { index: number; lotNo: string; message: string }[];
  }> {
    const created: any[] = [];
    const errors: { index: number; lotNo: string; message: string }[] = [];

    // Pre-validate every lot BEFORE touching the database so we can report
    // all validation errors at once without a partial write.
    const validRecords: { index: number; data: any }[] = [];

    for (let i = 0; i < purchases.length; i++) {
      const data = purchases[i];
      try {
        const availability = await this.checkLotAvailability(data.lotNo, data.vegetableId);
        if (!availability.isAvailable) {
          errors.push({
            index: i,
            lotNo: data.lotNo,
            message: availability.message,
          });
          continue;
        }

        validRecords.push({ index: i, data });
      } catch (err: any) {
        errors.push({
          index: i,
          lotNo: data.lotNo ?? `record[${i}]`,
          message: err instanceof Error ? err.message : "Validation failed",
        });
      }
    }

    if (validRecords.length === 0) {
      return { created, errors };
    }

    // Process all valid records inside a single transaction
    const results = await prisma.$transaction(async (tx) => {
      const txCreated: any[] = [];

      for (const { data } of validRecords) {
        const totals = calculatePurchaseTotals({
          unit: data.unit,
          rateType: data.rateType,
          quantity: data.quantity,
          bags: data.bags,
          ratePerUnit: data.ratePerUnit,
          transportFee: data.transportFee,
          loadingUnloadingFee: data.loadingUnloadingFee,
          otherCharges: data.otherCharges,
          thaiyalCharges: data.thaiyalCharges,
          extraCharges: data.extraCharges,
          advanceAmount: data.advanceAmount,
        });

        const { expenseAmount, expensePaymentMode, ...purchaseData } = data;

        const createDataPayload = {
          ...purchaseData,
          ...totals,
          quantity: totals.quantity,
          bags: totals.bags,
          remainingQuantity: totals.quantity,
          remainingBags: totals.bags,
          status: "ACTIVE" as const,
        };

        const purchase = await tx.purchase.create({ data: createDataPayload });

        if (purchase.supplierId) {
          await tx.supplier.update({
            where: { id: purchase.supplierId },
            data: { outstandingBalance: { increment: purchase.balanceAmount } },
          });
        }

        // Handle extra purchase expense creating an Expense record
        if (data.expenseAmount && Number(data.expenseAmount) > 0 && data.expensePaymentMode) {
          await tx.expense.create({
            data: {
              expenseDate: data.purchaseDate ? new Date(data.purchaseDate) : new Date(),
              category: "Purchase Expense",
              amount: new Prisma.Decimal(data.expenseAmount),
              paymentMode: data.expensePaymentMode,
              purchaseId: purchase.id,
              remarks: `Purchase Order ID: ${purchase.id}, Invoice: ${purchase.billNo || 'N/A'}, Lot: ${purchase.lotNo}`,
            },
          });
        }

        txCreated.push(purchase);
      }

      return txCreated;
    });

    created.push(...results);
    return { created, errors };
  }

  async closePurchase(id: bigint) {
    const purchase = await this.purchaseRepository.findById(id);
    if (!purchase) throw new Error("Purchase not found");
    if (purchase.status === "CLOSED") throw new Error("Purchase is already closed");

    return await prisma.purchase.update({
      where: { id },
      data: {
        status: "CLOSED",
        remainingQuantity: 0,
        remainingBags: 0,
        closedAt: new Date(),
      },
    });
  }

  async getPurchases(
    page: number = 1,
    limit: number = 10,
    search?: string,
    dateFromStr?: string,
    dateToStr?: string,
    supplierId?: string,
    status?: string
  ) {
    const skip = (page - 1) * limit;

    let dateFrom: Date | undefined;
    let dateTo: Date | undefined;

    if (dateFromStr) dateFrom = new Date(dateFromStr);
    if (dateToStr) dateTo = new Date(dateToStr);

    const { items, total } = await this.purchaseRepository.findAll(
      skip,
      limit,
      search,
      dateFrom,
      dateTo,
      supplierId ? BigInt(supplierId) : undefined,
      undefined,
      status
    );

    return {
      data: items,
      meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
    };
  }

  async getPurchaseById(id: bigint) {
    const purchase = await this.purchaseRepository.findById(id);
    if (!purchase) throw new Error("Purchase not found");
    return purchase;
  }

  async updatePurchase(id: bigint, data: any) {
    const isSold = await this.purchaseRepository.hasSaleItems(id);
    if (isSold) {
      throw new Error("This purchase lot has been used in sales and cannot be edited.");
    }

    return await prisma.$transaction(async (tx) => {
      // Find existing inside transaction
      const existing = await tx.purchase.findUnique({ where: { id } });
      if (!existing) throw new Error("Purchase not found");

      const { expenseAmount, expensePaymentMode, ...purchaseData } = data;

      if (purchaseData.lotNo && purchaseData.lotNo !== existing.lotNo) {
        // If changing lotNo, check if the NEW lotNo is available and belongs to the same vegetable
        const targetVegId = purchaseData.vegetableId ? BigInt(purchaseData.vegetableId) : existing.vegetableId;
        const availability = await this.checkLotAvailability(purchaseData.lotNo, targetVegId);
        if (!availability.isAvailable) {
          throw new Error(availability.message);
        }
      }

      // Merge existing data with updates for full recalculation
      const mergedData = { ...existing, ...purchaseData };

      // Derive what's already been sold/allocated/spoiled
      const alreadyConsumed = Number(existing.quantity) - Number(existing.remainingQuantity);
      const bagsAlreadyConsumed = Number(existing.bags) - Number(existing.remainingBags);

      const newQuantity = Number(mergedData.quantity ?? existing.quantity);
      const newBags = Number(mergedData.bags ?? existing.bags);

      // GUARD — cannot reduce below what's already gone
      if (newQuantity < alreadyConsumed) {
        throw new Error(`Cannot reduce quantity to ${newQuantity}; ${alreadyConsumed} already consumed`);
      }
      if (newBags < bagsAlreadyConsumed) {
        throw new Error(`Cannot reduce bags to ${newBags}; ${bagsAlreadyConsumed} already consumed`);
      }

      const totals = calculatePurchaseTotals({
        unit: mergedData.unit,
        rateType: mergedData.rateType,
        quantity: mergedData.quantity,
        bags: mergedData.bags,
        ratePerUnit: mergedData.ratePerUnit,
        transportFee: mergedData.transportFee,
        loadingUnloadingFee: mergedData.loadingUnloadingFee,
        otherCharges: mergedData.otherCharges,
        thaiyalCharges: mergedData.thaiyalCharges,
        extraCharges: mergedData.extraCharges,
        advanceAmount: mergedData.advanceAmount,
      });

      // Remaining = new total minus already consumed
      const newRemainingQuantity = totals.quantity.sub(new Prisma.Decimal(alreadyConsumed));
      const newRemainingBags = totals.bags.sub(new Prisma.Decimal(bagsAlreadyConsumed));

      const updateDataPayload = {
        ...purchaseData,
        ...totals,
        quantity: totals.quantity,
        bags: totals.bags,
        remainingQuantity: newRemainingQuantity,
        remainingBags: newRemainingBags,
      };

      const updatedPurchase = await tx.purchase.update({
        where: { id },
        data: updateDataPayload,
      });

      // Adjust supplier outstanding balance by the balanceAmount delta
      const oldBalance = new Prisma.Decimal(existing.balanceAmount);
      const newBalance = new Prisma.Decimal(updatedPurchase.balanceAmount);
      const delta = newBalance.sub(oldBalance);

      if (!delta.isZero() && updatedPurchase.supplierId) {
        await tx.supplier.update({
          where: { id: updatedPurchase.supplierId },
          data: { outstandingBalance: { increment: delta } },
        });
      }

      // Handle Expense update
      if (expenseAmount !== undefined && expensePaymentMode !== undefined) {
        const existingExpense = await tx.expense.findFirst({
          where: { purchaseId: updatedPurchase.id, category: "Purchase Expense" },
        });

        if (expenseAmount !== null && Number(expenseAmount) > 0 && expensePaymentMode) {
          if (existingExpense) {
            await tx.expense.update({
              where: { id: existingExpense.id },
              data: {
                amount: new Prisma.Decimal(expenseAmount),
                paymentMode: expensePaymentMode,
              },
            });
          } else {
            await tx.expense.create({
              data: {
                expenseDate: purchaseData.purchaseDate ? new Date(purchaseData.purchaseDate) : new Date(),
                category: "Purchase Expense",
                amount: new Prisma.Decimal(expenseAmount),
                paymentMode: expensePaymentMode,
                purchaseId: updatedPurchase.id,
                remarks: `Purchase Order ID: ${updatedPurchase.id}, Invoice: ${updatedPurchase.billNo || 'N/A'}, Lot: ${updatedPurchase.lotNo}`,
              },
            });
          }
        } else if (existingExpense) {
          await tx.expense.delete({
            where: { id: existingExpense.id },
          });
        }
      }

      return updatedPurchase;
    });
  }

  async deletePurchase(id: bigint) {
    const existing = await this.purchaseRepository.findById(id);
    if (!existing) throw new Error("Purchase not found");

    // Block delete if lot has been used in any sale
    const isSold = await this.purchaseRepository.hasSaleItems(id);
    if (isSold) {
      throw new Error(
        "This purchase lot has been used in sales and cannot be deleted."
      );
    }

    return await prisma.$transaction(async (tx) => {
      const deleted = await tx.purchase.update({
        where: { id },
        data: { deletedAt: new Date() },
      });

      // Revert supplier outstanding balance if supplier exists
      if (deleted.supplierId) {
        await tx.supplier.update({
          where: { id: deleted.supplierId },
          data: { outstandingBalance: { decrement: deleted.balanceAmount } },
        });
      }

      // Hard delete associated expense since Expense has no soft delete
      await tx.expense.deleteMany({
        where: { purchaseId: id, category: "Purchase Expense" },
      });

      return deleted;
    });
  }

  async getPurchaseMeta() {
    const [suppliers, vegetables] = await Promise.all([
      prisma.supplier.findMany({
        where: { status: true, deletedAt: null },
        select: { id: true, name: true,outstandingBalance:true },
        orderBy: { name: "asc" },
      }),
      prisma.vegetable.findMany({
        where: { status: true },
        select: { id: true, name: true, isFlexible: true },
        orderBy: { name: "asc" },
      }),
    ]);
    return { suppliers, vegetables };
  }
}
