import { SaleRepository } from "../repositories/SaleRepository.js";
import { PurchaseRepository } from "../repositories/PurchaseRepository.js";
import prisma from "../config/prisma.js";
import { Prisma } from "../generated/prisma/client.js";
import { InvoicePdf } from "../utils/pdf/InvoicePdf.js";
import type { Response } from "express";
import { ShopRepository } from "../repositories/ShopRepository.js";
import { UpiRepository } from "../repositories/UpiRepository.js";
import { calculateSaleItem } from "../utils/saleCalculations.js";
import whatsappService from "./whatsapp/index.js";

export class SaleService {
  constructor(
    private readonly saleRepository = new SaleRepository(),
    private readonly shopRepository = new ShopRepository(),
    private readonly upiRepository = new UpiRepository(),
    private readonly purchaseRepository = new PurchaseRepository()
  ) { }

  // ── Invoice No Generator ────────────────────────────────────────────────────
  // Generates a candidate invoice number. Uniqueness is enforced atomically
  // inside the createSale transaction (unique DB constraint), so there is no
  // race-condition window between the check and the INSERT.
  private generateInvoiceNoCandidate(): string {
    const today = new Date();
    const datePart = today.toISOString().slice(0, 10).replace(/-/g, ""); // YYYYMMDD
    const rand = Math.floor(10000 + Math.random() * 90000); // 5-digit random
    return `INV-${datePart}-${rand}`;
  }

  async createSale(data: any) {
    const { items, ...saleFields } = data;

    // Precedence rule: if customerId is present, treat as registered customer and clear walk-in details
    const isWalkIn = !saleFields.customerId;
    if (!isWalkIn) {
      saleFields.walkInName = null;
      saleFields.walkInMobile = null;
    }

    // ── Step 1: Validate customer (if not walk-in) ────────────────────────────
    if (!isWalkIn) {
      const customer = await prisma.customer.findUnique({
        where: { id: saleFields.customerId },
      });
      if (!customer) throw new Error("Customer not found");
    }

    if (!items || items.length === 0) {
      throw new Error("At least one sale item is required.");
    }

    const createdSale = await prisma.$transaction(async (tx) => {
      let invoiceNo: string;
      do {
        invoiceNo = this.generateInvoiceNoCandidate();
        const existing = await tx.sale.findUnique({ where: { invoiceNo } });
        if (!existing) break;
      } while (true);
      saleFields.invoiceNo = invoiceNo;

      const validatedItems: any[] = [];

      for (const item of items) {
        if (item.purchaseId) {
          // Direct purchase batch deduction (Option 2)
          const purchase = await tx.purchase.findFirst({
            where: { id: BigInt(item.purchaseId), deletedAt: null },
            include: { arrivalSettlement: true, vegetable: true },
          });

          if (!purchase) {
            throw new Error(`Selected purchase batch not found (id: ${item.purchaseId}).`);
          }

          if (
            purchase.purchaseType === "COMMISSION_ARRIVAL" &&
            purchase.arrivalSettlement?.status === "SETTLED"
          ) {
            throw new Error(`Cannot record sale against Arrival Lot "${purchase.lotNo}" (${purchase.vegetable?.name || 'vegetable'}) because it is already settled.`);
          }

          if (purchase.status === "CLOSED") {
            throw new Error(`Lot batch ${purchase.lotNo} (${purchase.vegetable?.name || 'vegetable'}) is CLOSED and cannot be sold.`);
          }

          const isFlexible = !!purchase.vegetable?.isFlexible;

          if (!isFlexible && purchase.rateType === "KG" && item.saleUnit !== "KG") {
            throw new Error(`Lot batch ${purchase.lotNo} (${purchase.vegetable?.name || 'vegetable'}) was purchased at Rate/Kg and must be sold by KG.`);
          }

          const calc = calculateSaleItem({
            saleUnit: item.saleUnit ?? "KG",
            quantity: item.quantity,
            ...(item.bags !== undefined && item.bags !== null ? { bags: item.bags } : {}),
            sellingRate: item.sellingRate,
            vegetableName: purchase.vegetable?.name,
            lotNo: purchase.lotNo,
            purchase: {
              remainingQuantity: purchase.remainingQuantity,
              remainingBags: purchase.remainingBags,
              weightPerBag: purchase.weightPerBag,
              costPerUnit: purchase.costPerUnit,
              rateType: purchase.rateType,
              isFlexible,
              vegetableName: purchase.vegetable?.name,
              lotNo: purchase.lotNo,
            },
          });

          if (isFlexible) {
            if (item.saleUnit === "THAAR" || item.saleUnit === "BAG") {
              const bagQtyToDeduct = calc.bags !== null ? calc.bags : calc.quantity;
              if (purchase.remainingBags.gt(0)) {
                await tx.purchase.update({
                  where: { id: purchase.id },
                  data: {
                    remainingBags: { decrement: Prisma.Decimal.min(purchase.remainingBags, bagQtyToDeduct) },
                  },
                });
              }
            } else if (item.saleUnit === "KG") {
              if (purchase.remainingQuantity.gt(0)) {
                const qtyToDeduct = Prisma.Decimal.min(purchase.remainingQuantity, calc.quantity);
                await tx.purchase.update({
                  where: { id: purchase.id },
                  data: {
                    remainingQuantity: { decrement: qtyToDeduct },
                  },
                });
              }
            }
          } else {
            let deductQty = calc.quantity;
            if (calc.bags !== null && new Prisma.Decimal(calc.bags).eq(purchase.remainingBags)) {
              deductQty = purchase.remainingQuantity;
            }

            const result = await tx.purchase.updateMany({
              where: {
                id: purchase.id,
                remainingQuantity: { gte: deductQty },
                ...(calc.bags !== null ? { remainingBags: { gte: calc.bags } } : {}),
              },
              data: {
                remainingQuantity: { decrement: deductQty },
                ...(calc.bags !== null ? { remainingBags: { decrement: calc.bags } } : {}),
              },
            });

            if (result.count === 0) {
              throw new Error(`Stock changed concurrently or insufficient stock for ${purchase.vegetable?.name ? `"${purchase.vegetable.name}" (lot ${purchase.lotNo})` : `lot ${purchase.lotNo}`}. Please retry.`);
            }

            await this.purchaseRepository.autoUpdateStatus(purchase.id, tx);
          }

          validatedItems.push({
            purchaseId: purchase.id,
            saleUnit: item.saleUnit ?? "KG",
            quantity: calc.quantity,
            bags: calc.bags,
            costPerUnit: calc.costPerUnit,
            sellingRate: calc.sellingRate,
            total: calc.total,
          });
        } else if (item.lotNo) {
          // FIFO Fallback if lotNo is supplied without purchaseId
          const activePurchases = await this.purchaseRepository.findActivePurchasesByLotNo(item.lotNo, tx);

          if (activePurchases.length === 0) {
            throw new Error(`No active stock found for lot "${item.lotNo}".`);
          }

          let qtyRemainingToDeduct = new Prisma.Decimal(item.quantity);

          for (const purchase of activePurchases) {
            if (qtyRemainingToDeduct.lte(0)) break;

            const availableQty = purchase.remainingQuantity;
            const portionQty = qtyRemainingToDeduct.lte(availableQty) ? qtyRemainingToDeduct : availableQty;

            let portionBags: Prisma.Decimal | null = null;
            if (item.saleUnit === "BAG" || (item.bags !== undefined && item.bags !== null)) {
              if (purchase.weightPerBag && purchase.weightPerBag.gt(0)) {
                portionBags = portionQty
                  .div(purchase.weightPerBag)
                  .toDecimalPlaces(3, Prisma.Decimal.ROUND_HALF_UP);
              } else if (item.bags) {
                portionBags = new Prisma.Decimal(item.bags);
              }
            }

            const calc = calculateSaleItem({
              saleUnit: item.saleUnit ?? "KG",
              quantity: portionQty,
              ...(portionBags !== null && portionBags !== undefined ? { bags: portionBags } : {}),
              sellingRate: item.sellingRate,
              vegetableName: purchase.vegetable?.name,
              lotNo: purchase.lotNo,
              purchase: {
                remainingQuantity: purchase.remainingQuantity,
                remainingBags: purchase.remainingBags,
                weightPerBag: purchase.weightPerBag,
                costPerUnit: purchase.costPerUnit,
                rateType: purchase.rateType,
                vegetableName: purchase.vegetable?.name,
                lotNo: purchase.lotNo,
              },
            });

            let deductQty = calc.quantity;
            if (calc.bags !== null && new Prisma.Decimal(calc.bags).eq(purchase.remainingBags)) {
              deductQty = purchase.remainingQuantity;
            }

            const result = await tx.purchase.updateMany({
              where: {
                id: purchase.id,
                remainingQuantity: { gte: deductQty },
                ...(calc.bags !== null ? { remainingBags: { gte: calc.bags } } : {}),
              },
              data: {
                remainingQuantity: { decrement: deductQty },
                ...(calc.bags !== null ? { remainingBags: { decrement: calc.bags } } : {}),
              },
            });

            if (result.count === 0) {
              throw new Error(`Stock changed concurrently or insufficient stock for lot "${item.lotNo}". Please retry.`);
            }

            await this.purchaseRepository.autoUpdateStatus(purchase.id, tx);

            validatedItems.push({
              purchaseId: purchase.id,
              saleUnit: item.saleUnit ?? "KG",
              quantity: calc.quantity,
              bags: calc.bags,
              costPerUnit: calc.costPerUnit,
              sellingRate: calc.sellingRate,
              total: calc.total,
            });

            qtyRemainingToDeduct = qtyRemainingToDeduct.sub(calc.quantity);
          }

          if (qtyRemainingToDeduct.gt(0)) {
            throw new Error(`Insufficient available stock for lot "${item.lotNo}". Short by ${qtyRemainingToDeduct} KG.`);
          }
        } else {
          throw new Error("Either purchaseId or lotNo must be provided for sale items.");
        }
      }

      const subTotal = validatedItems
        .reduce((acc, i) => acc.add(i.total), new Prisma.Decimal(0))
        .toDecimalPlaces(2, Prisma.Decimal.ROUND_HALF_UP);

      const discount = new Prisma.Decimal(saleFields.discount ?? 0).toDecimalPlaces(2);
      const freightCharge = new Prisma.Decimal(saleFields.freightCharge ?? 0).toDecimalPlaces(2);
      const grandTotal = subTotal
        .sub(discount)
        .add(freightCharge)
        .toDecimalPlaces(2, Prisma.Decimal.ROUND_HALF_UP);
      // ── Payment breakdown → derive receivedAmount ─────────────────────────
      const cashAmount = new Prisma.Decimal(saleFields.cashAmount ?? 0).toDecimalPlaces(2);
      const upiAmount = new Prisma.Decimal(saleFields.upiAmount ?? 0).toDecimalPlaces(2);
      const bankAmount = new Prisma.Decimal(saleFields.bankAmount ?? 0).toDecimalPlaces(2);
      let receivedAmount: Prisma.Decimal;
      if (isWalkIn) {
        const computedReceived = cashAmount.add(upiAmount).add(bankAmount);
        if (
          computedReceived.gt(0) &&
          !computedReceived.eq(grandTotal)
        ) {
          throw new Error("Walk-in sales must be paid in full at the time of sale");
        }
        receivedAmount = grandTotal;
      } else {
        receivedAmount = cashAmount.add(upiAmount).add(bankAmount);
      }



      if (isWalkIn && receivedAmount.gt(grandTotal)) {
        throw new Error(
          `Received amount cannot exceed the grand total for walk-in sales.`
        );
      }

      const balanceAmount = grandTotal.sub(receivedAmount).toDecimalPlaces(2, Prisma.Decimal.ROUND_HALF_UP);

      let paymentStatus: "UNPAID" | "PARTIALLY_PAID" | "PAID";
      if (balanceAmount.lte(0)) {
        paymentStatus = "PAID";
      } else if (receivedAmount.gt(0)) {
        paymentStatus = "PARTIALLY_PAID";
      } else {
        paymentStatus = "UNPAID";
      }

      const sale = await tx.sale.create({
        data: {
          invoiceNo: saleFields.invoiceNo,
          customerId: isWalkIn ? null : saleFields.customerId,
          walkInName: isWalkIn ? saleFields.walkInName : null,
          walkInMobile: isWalkIn ? saleFields.walkInMobile ?? null : null,
          invoiceDate: new Date(saleFields.invoiceDate),
          subTotal,
          discount,
          freightCharge,
          grandTotal,
          receivedAmount,
          cashAmount,
          upiAmount,
          bankAmount,
          balanceAmount,
          paymentStatus,
          remarks: saleFields.remarks,
          images: saleFields.images ?? [],
          createdBy: saleFields.createdBy ? BigInt(saleFields.createdBy) : null,
          items: {
            create: validatedItems.map((item) => ({
              ...item,
              createdBy: saleFields.createdBy ? BigInt(saleFields.createdBy) : null,
            })),
          },
        },
        include: {
          items: true,
          customer: { select: { name: true } },
        },
      });

      if (!isWalkIn && !balanceAmount.isZero()) {
        const oldCustomer = await tx.customer.findUnique({ where: { id: saleFields.customerId } });
        
        const updatedCustomer = await tx.customer.update({
          where: { id: saleFields.customerId },
          data: { outstandingBalance: { increment: balanceAmount } },
        });

        if (oldCustomer) {
          await tx.auditLog.create({
            data: {
              entityName: "Customer",
              entityId: saleFields.customerId,
              action: "UPDATE",
              oldValues: { outstandingBalance: oldCustomer.outstandingBalance.toNumber() },
              newValues: { outstandingBalance: updatedCustomer.outstandingBalance.toNumber() },
              userId: saleFields.createdBy ? BigInt(saleFields.createdBy) : null,
            }
          });
        }
      }

      return sale;
    });

    return createdSale;
  }

  async updateSale(id: bigint, data: any) {
    const { items, ...saleFields } = data;

    const existingSale = await prisma.sale.findUnique({
      where: { id },
      include: { 
        items: {
          include: { 
            saleReturns: true,
            purchase: {
              include: {
                arrivalSettlement: true,
              },
            },
          },
        },
      },
    });

    if (!existingSale) throw new Error("Sale not found");
    if (existingSale.deletedAt) throw new Error("Cancelled sale cannot be edited");
    
    const hasReturns = existingSale.items.some((item: any) => item.saleReturns && item.saleReturns.length > 0);
    if (hasReturns) {
      throw new Error("Cannot edit a sale that already has returns. Please cancel and create a new one, or manage returns instead.");
    }

    // Check settled arrival lots:
    // If an arrival lot is settled, the user cannot edit or remove that arrival lot item.
    // However, they CAN edit other records (e.g. direct purchases) as long as the settled arrival lot item is unchanged.
    const settledOldItems = existingSale.items.filter((oldItem: any) =>
      oldItem.purchase?.purchaseType === "COMMISSION_ARRIVAL" &&
      oldItem.purchase?.arrivalSettlement?.status === "SETTLED"
    );

    const matchedSubmittedItemIndices = new Set<number>();
    const unchangedSettledOldItemIds = new Set<string>();

    for (const settledOldItem of settledOldItems) {
      const matchingIndex = (items || []).findIndex((submittedItem: any, idx: number) =>
        !matchedSubmittedItemIndices.has(idx) &&
        BigInt(submittedItem.purchaseId) === BigInt(settledOldItem.purchaseId)
      );

      if (matchingIndex === -1) {
        throw new Error(
          `Cannot remove Arrival Lot "${settledOldItem.purchase?.lotNo || ''}" because it is already settled.`
        );
      }

      const submittedItem = items[matchingIndex];
      const oldQty = new Prisma.Decimal(settledOldItem.quantity || 0);
      const newQty = new Prisma.Decimal(submittedItem.quantity ?? 0);
      const oldBagsVal = settledOldItem.bags !== null && settledOldItem.bags !== undefined ? Number(settledOldItem.bags) : 0;
      const newBagsVal = submittedItem.bags !== null && submittedItem.bags !== undefined ? Number(submittedItem.bags) : 0;
      const oldRate = new Prisma.Decimal(settledOldItem.sellingRate || 0);
      const newRate = new Prisma.Decimal(submittedItem.sellingRate ?? 0);
      const unitMatches = (settledOldItem.saleUnit || "KG") === (submittedItem.saleUnit || "KG");

      if (!oldQty.eq(newQty) || oldBagsVal !== newBagsVal || !oldRate.eq(newRate) || !unitMatches) {
        throw new Error(
          `Cannot edit Arrival Lot "${settledOldItem.purchase?.lotNo || ''}" because it is already settled.`
        );
      }

      matchedSubmittedItemIndices.add(matchingIndex);
      unchangedSettledOldItemIds.add(String(settledOldItem.id));
    }

    const isWalkIn = !saleFields.customerId;
    if (!isWalkIn) {
      saleFields.walkInName = null;
      saleFields.walkInMobile = null;
      const customer = await prisma.customer.findUnique({
        where: { id: saleFields.customerId },
      });
      if (!customer) throw new Error("Customer not found");
    }

    if (!items || items.length === 0) {
      throw new Error("At least one sale item is required.");
    }

    return await prisma.$transaction(async (tx) => {
      // 1. Revert Stock (skip settled arrival lot items that are unchanged)
      for (const oldItem of existingSale.items) {
        if (unchangedSettledOldItemIds.has(String(oldItem.id))) {
          continue;
        }

        if (oldItem.purchaseId) {
          const oldPurchase = await tx.purchase.findUnique({
            where: { id: oldItem.purchaseId },
            include: { vegetable: true },
          });
          const isFlex = !!oldPurchase?.vegetable?.isFlexible;
          if (isFlex && (oldItem.saleUnit === "THAAR" || oldPurchase?.unit === "THAAR")) {
            const thaarCount = (oldItem.quantity && !oldItem.quantity.isZero()) ? oldItem.quantity : (oldItem.bags ?? new Prisma.Decimal(0));
            await tx.purchase.update({
              where: { id: oldItem.purchaseId },
              data: {
                remainingBags: { increment: thaarCount },
              },
            });
          } else {
            await tx.purchase.update({
              where: { id: oldItem.purchaseId },
              data: {
                remainingQuantity: { increment: oldItem.quantity },
                ...(oldItem.bags ? { remainingBags: { increment: oldItem.bags } } : {}),
              },
            });
          }
          await this.purchaseRepository.autoUpdateStatus(oldItem.purchaseId, tx);
        }
      }

      // 2. Revert Customer Balance
      if (existingSale.customerId && !existingSale.balanceAmount.isZero()) {
        const oldCustomer = await tx.customer.findUnique({ where: { id: existingSale.customerId } });
        
        const updatedCustomer = await tx.customer.update({
          where: { id: existingSale.customerId },
          data: { outstandingBalance: { decrement: existingSale.balanceAmount } },
        });

        if (oldCustomer) {
          await tx.auditLog.create({
            data: {
              entityName: "Customer",
              entityId: existingSale.customerId,
              action: "UPDATE",
              oldValues: { outstandingBalance: oldCustomer.outstandingBalance.toNumber() },
              newValues: { outstandingBalance: updatedCustomer.outstandingBalance.toNumber() },
              userId: saleFields.createdBy ? BigInt(saleFields.createdBy) : null,
            }
          });
        }
      }

      // 3. Delete old items
      await tx.saleItem.deleteMany({ where: { saleId: id } });

      // 4. Validate and Apply New Items
      const validatedItems: any[] = [];
      for (let idx = 0; idx < items.length; idx++) {
        const item = items[idx];
        const isUnchangedSettled = matchedSubmittedItemIndices.has(idx);

        if (item.purchaseId) {
          const purchase = await tx.purchase.findFirst({
            where: { id: BigInt(item.purchaseId), deletedAt: null },
            include: { arrivalSettlement: true, vegetable: true },
          });

          if (!purchase) {
            throw new Error(`Selected purchase batch not found (id: ${item.purchaseId}).`);
          }

          if (
            purchase.purchaseType === "COMMISSION_ARRIVAL" &&
            purchase.arrivalSettlement?.status === "SETTLED" &&
            !isUnchangedSettled
          ) {
            throw new Error(`Cannot record sale against Arrival Lot "${purchase.lotNo}" (${purchase.vegetable?.name || 'vegetable'}) because it is already settled.`);
          }

          if (purchase.status === "CLOSED" && !isUnchangedSettled) {
            throw new Error(`Lot batch ${purchase.lotNo} (${purchase.vegetable?.name || 'vegetable'}) is CLOSED and cannot be sold.`);
          }

          const isFlexible = !!purchase.vegetable?.isFlexible;

          if (!isFlexible && purchase.rateType === "KG" && item.saleUnit !== "KG") {
            throw new Error(`Lot batch ${purchase.lotNo} (${purchase.vegetable?.name || 'vegetable'}) was purchased at Rate/Kg and must be sold by KG.`);
          }

          if (isUnchangedSettled) {
            const oldItem = existingSale.items.find(
              (oi: any) => unchangedSettledOldItemIds.has(String(oi.id)) && BigInt(oi.purchaseId) === BigInt(item.purchaseId)
            );

            const calcTotal = new Prisma.Decimal(item.sellingRate)
              .mul(new Prisma.Decimal(item.quantity))
              .toDecimalPlaces(2, Prisma.Decimal.ROUND_HALF_UP);

            validatedItems.push({
              purchaseId: purchase.id,
              saleUnit: item.saleUnit ?? "KG",
              quantity: new Prisma.Decimal(item.quantity),
              bags: item.bags !== undefined && item.bags !== null ? new Prisma.Decimal(item.bags) : null,
              costPerUnit: oldItem ? oldItem.costPerUnit : purchase.costPerUnit,
              sellingRate: new Prisma.Decimal(item.sellingRate),
              total: calcTotal,
            });
            continue;
          }

          const calc = calculateSaleItem({
            saleUnit: item.saleUnit ?? "KG",
            quantity: item.quantity,
            ...(item.bags !== undefined && item.bags !== null ? { bags: item.bags } : {}),
            sellingRate: item.sellingRate,
            vegetableName: purchase.vegetable?.name,
            lotNo: purchase.lotNo,
            purchase: {
              remainingQuantity: purchase.remainingQuantity,
              remainingBags: purchase.remainingBags,
              weightPerBag: purchase.weightPerBag,
              costPerUnit: purchase.costPerUnit,
              rateType: purchase.rateType,
              isFlexible,
              vegetableName: purchase.vegetable?.name,
              lotNo: purchase.lotNo,
            },
          });

          if (isFlexible) {
            if (item.saleUnit === "THAAR" || item.saleUnit === "BAG") {
              const bagQtyToDeduct = calc.bags !== null ? calc.bags : calc.quantity;
              if (purchase.remainingBags.gt(0)) {
                await tx.purchase.update({
                  where: { id: purchase.id },
                  data: {
                    remainingBags: { decrement: Prisma.Decimal.min(purchase.remainingBags, bagQtyToDeduct) },
                  },
                });
              }
            } else if (item.saleUnit === "KG") {
              if (purchase.remainingQuantity.gt(0)) {
                const qtyToDeduct = Prisma.Decimal.min(purchase.remainingQuantity, calc.quantity);
                await tx.purchase.update({
                  where: { id: purchase.id },
                  data: {
                    remainingQuantity: { decrement: qtyToDeduct },
                  },
                });
              }
            }
          } else {
            let deductQty = calc.quantity;
            if (calc.bags !== null && new Prisma.Decimal(calc.bags).eq(purchase.remainingBags)) {
              deductQty = purchase.remainingQuantity;
            }

            const result = await tx.purchase.updateMany({
              where: {
                id: purchase.id,
                remainingQuantity: { gte: deductQty },
                ...(calc.bags !== null ? { remainingBags: { gte: calc.bags } } : {}),
              },
              data: {
                remainingQuantity: { decrement: deductQty },
                ...(calc.bags !== null ? { remainingBags: { decrement: calc.bags } } : {}),
              },
            });

            if (result.count === 0) {
              throw new Error(`Insufficient stock for ${purchase.vegetable?.name ? `"${purchase.vegetable.name}" (lot ${purchase.lotNo})` : `lot ${purchase.lotNo}`}. Please retry.`);
            }

            await this.purchaseRepository.autoUpdateStatus(purchase.id, tx);
          }

          validatedItems.push({
            purchaseId: purchase.id,
            saleUnit: item.saleUnit ?? "KG",
            quantity: calc.quantity,
            bags: calc.bags,
            costPerUnit: calc.costPerUnit,
            sellingRate: calc.sellingRate,
            total: calc.total,
          });
        } else {
          throw new Error("purchaseId is required for sale items during update.");
        }
      }

      // 5. Calculate new totals
      const subTotal = validatedItems
        .reduce((acc, i) => acc.add(i.total), new Prisma.Decimal(0))
        .toDecimalPlaces(2, Prisma.Decimal.ROUND_HALF_UP);

      const discount = new Prisma.Decimal(saleFields.discount ?? 0).toDecimalPlaces(2);
      const freightCharge = new Prisma.Decimal(saleFields.freightCharge ?? 0).toDecimalPlaces(2);
      const grandTotal = subTotal
        .sub(discount)
        .add(freightCharge)
        .toDecimalPlaces(2, Prisma.Decimal.ROUND_HALF_UP);

      const cashAmount = new Prisma.Decimal(saleFields.cashAmount ?? 0).toDecimalPlaces(2);
      const upiAmount = new Prisma.Decimal(saleFields.upiAmount ?? 0).toDecimalPlaces(2);
      const bankAmount = new Prisma.Decimal(saleFields.bankAmount ?? 0).toDecimalPlaces(2);
      let receivedAmount: Prisma.Decimal;
      
      if (isWalkIn) {
        const computedReceived = cashAmount.add(upiAmount).add(bankAmount);
        if (computedReceived.gt(0) && !computedReceived.eq(grandTotal)) {
          throw new Error("Walk-in sales must be paid in full at the time of sale");
        }
        receivedAmount = grandTotal;
      } else {
        receivedAmount = cashAmount.add(upiAmount).add(bankAmount);
      }

      if (isWalkIn && receivedAmount.gt(grandTotal)) {
        throw new Error(`Received amount cannot exceed the grand total for walk-in sales.`);
      }

      const balanceAmount = grandTotal.sub(receivedAmount).toDecimalPlaces(2, Prisma.Decimal.ROUND_HALF_UP);

      let paymentStatus: "UNPAID" | "PARTIALLY_PAID" | "PAID";
      if (balanceAmount.lte(0)) {
        paymentStatus = "PAID";
      } else if (receivedAmount.gt(0)) {
        paymentStatus = "PARTIALLY_PAID";
      } else {
        paymentStatus = "UNPAID";
      }

      // 6. Update Sale
      const updatedSale = await tx.sale.update({
        where: { id },
        data: {
          customerId: isWalkIn ? null : BigInt(saleFields.customerId),
          walkInName: isWalkIn ? saleFields.walkInName : null,
          walkInMobile: isWalkIn ? saleFields.walkInMobile ?? null : null,
          invoiceDate: new Date(saleFields.invoiceDate),
          subTotal,
          discount,
          freightCharge,
          grandTotal,
          receivedAmount,
          cashAmount,
          upiAmount,
          bankAmount,
          balanceAmount,
          paymentStatus,
          remarks: saleFields.remarks,
          images: saleFields.images ?? [],
          items: {
            create: validatedItems.map((item) => ({
              ...item,
              createdBy: saleFields.createdBy ? BigInt(saleFields.createdBy) : null,
            })),
          },
        },
        include: {
          items: true,
          customer: { select: { name: true } },
        },
      });

      // 7. Update new Customer Balance
      if (!isWalkIn && !balanceAmount.isZero()) {
        const oldCustomer = await tx.customer.findUnique({ where: { id: saleFields.customerId } });

        const updatedCustomer = await tx.customer.update({
          where: { id: BigInt(saleFields.customerId) },
          data: { outstandingBalance: { increment: balanceAmount } },
        });

        if (oldCustomer) {
          await tx.auditLog.create({
            data: {
              entityName: "Customer",
              entityId: saleFields.customerId,
              action: "UPDATE",
              oldValues: { outstandingBalance: oldCustomer.outstandingBalance.toNumber() },
              newValues: { outstandingBalance: updatedCustomer.outstandingBalance.toNumber() },
              userId: saleFields.createdBy ? BigInt(saleFields.createdBy) : null,
            }
          });
        }
      }

      return updatedSale;
    });
  }

  async cancelSale(id: bigint) {
    const sale = await this.saleRepository.findById(id);
    if (!sale) throw new Error("Sale not found");
    if (sale.deletedAt) throw new Error("Sale is already cancelled");

    const settledItem = sale.items.find((item: any) =>
      item.purchase?.purchaseType === "COMMISSION_ARRIVAL" &&
      item.purchase?.arrivalSettlement?.status === "SETTLED"
    );
    if (settledItem) {
      throw new Error(`Cannot cancel this sale — Arrival Lot "${settledItem.purchase?.lotNo || ''}" is already settled.`);
    }

    return await prisma.$transaction(async (tx) => {
      // Restore only the NET quantity still held from this sale.
      // SaleReturnService already incremented stock for every return processed,
      // so restoring item.quantity in full would over-restore by the returned qty.
      for (const item of sale.items) {
        const alreadyReturnedQty = (item.saleReturns || []).reduce(
          (acc: Prisma.Decimal, ret: any) => acc.add(new Prisma.Decimal(ret.quantity)),
          new Prisma.Decimal(0)
        );
        const netQtyToRestore = new Prisma.Decimal(item.quantity).sub(alreadyReturnedQty);

        // Nothing to restore if all quantity was already returned
        if (netQtyToRestore.lte(0)) continue;

        // Compute net bags proportionally (if the lot tracks bags)
        const purchase = await tx.purchase.findUnique({
          where: { id: item.purchaseId },
          select: { weightPerBag: true },
        });
        const netBagsToRestore =
          item.bags && purchase?.weightPerBag
            ? netQtyToRestore
              .div(new Prisma.Decimal(purchase.weightPerBag))
              .toDecimalPlaces(3, Prisma.Decimal.ROUND_HALF_UP)
            : null;

        await tx.purchase.update({
          where: { id: item.purchaseId },
          data: {
            remainingQuantity: { increment: netQtyToRestore },
            ...(netBagsToRestore !== null ? { remainingBags: { increment: netBagsToRestore } } : {}),
          },
        });

        // Auto update status
        const updatedPurchase = await tx.purchase.findUnique({
          where: { id: item.purchaseId },
          select: { remainingQuantity: true, status: true },
        });
        if (updatedPurchase && updatedPurchase.status !== "CLOSED") {
          const newStatus = updatedPurchase.remainingQuantity.gt(0) ? "ACTIVE" : "DEPLETED";
          if (updatedPurchase.status !== newStatus) {
            await tx.purchase.update({
              where: { id: item.purchaseId },
              data: { status: newStatus },
            });
          }
        }
      }

      // Restore customer outstanding balance
      if (sale.customerId && !sale.balanceAmount.isZero()) {
        const oldCustomer = await tx.customer.findUnique({ where: { id: sale.customerId } });

        const updatedCustomer = await tx.customer.update({
          where: { id: sale.customerId },
          data: { outstandingBalance: { decrement: sale.balanceAmount } },
        });

        if (oldCustomer) {
          await tx.auditLog.create({
            data: {
              entityName: "Customer",
              entityId: sale.customerId,
              action: "UPDATE",
              oldValues: { outstandingBalance: oldCustomer.outstandingBalance.toNumber() },
              newValues: { outstandingBalance: updatedCustomer.outstandingBalance.toNumber() },
              userId: sale.createdBy ? sale.createdBy : null,
            }
          });
        }
      }

      // Soft-delete the sale (marks as cancelled)
      return tx.sale.update({
        where: { id },
        data: { deletedAt: new Date() },
      });
    });
  }

  private enrichSaleWithReturns(sale: any) {
    if (!sale) return sale;

    let totalReturnedAmount = new Prisma.Decimal(0);

    const enrichedItems = (sale.items || []).map((item: any) => {
      const returnedQuantity = (item.saleReturns || []).reduce(
        (acc: Prisma.Decimal, ret: any) => acc.add(new Prisma.Decimal(ret.quantity)),
        new Prisma.Decimal(0)
      );
      const returnedAmount = (item.saleReturns || []).reduce(
        (acc: Prisma.Decimal, ret: any) => acc.add(new Prisma.Decimal(ret.returnAmount)),
        new Prisma.Decimal(0)
      );

      totalReturnedAmount = totalReturnedAmount.add(returnedAmount);

      let returnedBags = new Prisma.Decimal(0);
      let bags: Prisma.Decimal | null | undefined = item.bags;
      const isBagOnly = item.saleUnit !== 'KG' && new Prisma.Decimal(item.quantity).lte(0);

      if (isBagOnly) {
        returnedBags = returnedQuantity;
        if (item.bags !== null && item.bags !== undefined) {
          const netBags = new Prisma.Decimal(item.bags).sub(returnedBags);
          bags = netBags.lt(0) ? new Prisma.Decimal(0) : netBags;
        }
      } else if (item.bags !== null && item.bags !== undefined) {
        const weightPerBag = item.purchase?.weightPerBag
          ? new Prisma.Decimal(item.purchase.weightPerBag)
          : null;
        if (weightPerBag && weightPerBag.gt(0)) {
          returnedBags = returnedQuantity
            .div(weightPerBag)
            .toDecimalPlaces(3, Prisma.Decimal.ROUND_HALF_UP);
        } else if (item.quantity && new Prisma.Decimal(item.quantity).gt(0)) {
          returnedBags = returnedQuantity
            .mul(new Prisma.Decimal(item.bags))
            .div(new Prisma.Decimal(item.quantity))
            .toDecimalPlaces(3, Prisma.Decimal.ROUND_HALF_UP);
        }

        const netBags = new Prisma.Decimal(item.bags).sub(returnedBags);
        bags = netBags.lt(0) ? new Prisma.Decimal(0) : netBags;
      }

      return {
        ...item,
        returnedQuantity: isBagOnly ? new Prisma.Decimal(0) : returnedQuantity,
        returnedAmount,
        returnedBags,
        originalQuantity: item.quantity,
        originalBags: item.bags,
        originalTotal: item.total,
        quantity: isBagOnly ? new Prisma.Decimal(0) : new Prisma.Decimal(item.quantity).sub(returnedQuantity),
        bags,
        total: new Prisma.Decimal(item.total).sub(returnedAmount),
      };
    });

    // Recompute netSubTotal by summing already-adjusted item totals (avoids double-deduction)
    const netSubTotal = enrichedItems.reduce(
      (acc: Prisma.Decimal, item: any) => acc.add(new Prisma.Decimal(item.total)),
      new Prisma.Decimal(0)
    );
    // Preserve discount/freightCharge: effective net subtotal after discount cannot drop below 0
    const discount = new Prisma.Decimal(sale.discount ?? 0);
    const freightCharge = new Prisma.Decimal(sale.freightCharge ?? 0);
    const netSubTotalAfterDiscount = netSubTotal.sub(discount);
    const effectiveNetSubTotal = netSubTotalAfterDiscount.lt(0)
      ? new Prisma.Decimal(0)
      : netSubTotalAfterDiscount;
    const netGrandTotal = effectiveNetSubTotal
      .add(freightCharge)
      .toDecimalPlaces(2, Prisma.Decimal.ROUND_HALF_UP);

    return {
      ...sale,
      items: enrichedItems,
      totalReturnedAmount,
      originalSubTotal: sale.subTotal,
      originalGrandTotal: sale.grandTotal,
      subTotal: netSubTotal,
      grandTotal: netGrandTotal,
    };
  }

  async getSales(
    page: number = 1,
    limit: number = 10,
    customerId?: bigint,
    dateFromStr?: string,
    dateToStr?: string,
    search?: string,
    saleType?: 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, summary } = await this.saleRepository.findAll(
      skip,
      limit,
      customerId,
      dateFrom,
      dateTo,
      search,
      saleType
    );

    const enrichedItems = items.map((sale) => this.enrichSaleWithReturns(sale));

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

  async getSaleById(id: bigint) {
    const sale = await this.saleRepository.findById(id);
    if (!sale) throw new Error("Sale not found");
    return this.enrichSaleWithReturns(sale);
  }

  async getAvailableLots(vegetableId?: bigint) {
    return this.saleRepository.getAvailableLots(vegetableId);
  }

  async getSaleMeta() {
    const [customers, vegetables] = await Promise.all([
      prisma.customer.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 },
        orderBy: { name: "asc" },
      }),
    ]);
    return { customers, vegetables };
  }

  async getSalesForExport(
    dateFromStr?: string,
    dateToStr?: string,
    search?: string,
    saleType?: string
  ) {
    const where: Prisma.SaleWhereInput = { deletedAt: null };

    if (saleType === 'CREDIT') {
      where.customerId = { not: null };
    } else if (saleType === 'QUICK') {
      where.customerId = null;
    }

    if (search) {
      where.OR = [
        { invoiceNo: { contains: search } },
        { customer: { name: { contains: search } } },
        { walkInName: { contains: search } },
      ];
    }

    if (dateFromStr || dateToStr) {
      where.invoiceDate = {};
      if (dateFromStr) (where.invoiceDate as any).gte = new Date(dateFromStr);
      if (dateToStr) (where.invoiceDate as any).lte = new Date(dateToStr);
    }

    const sales = await prisma.sale.findMany({
      where,
      orderBy: { invoiceDate: "desc" },
      include: {
        customer: { select: { name: true } },
        items: {
          include: {
            saleReturns: true,
            purchase: {
              select: {
                lotNo: true,
                unit: true,
                weightPerBag: true,
                vegetable: { select: { name: true } },
              },
            },
          },
        },
      },
    });

    return sales.map((sale) => this.enrichSaleWithReturns(sale));
  }

  async generateInvoice(
    id: bigint,
    res: Response,
    options: { lang: string; printSign: boolean } = { lang: "en", printSign: false } // TEMP: Digital signature disabled — change to true to re-enable
  ): Promise<void> {
    const sale = await this.getSaleById(id);

    if (!sale) {
      throw new Error("Sale not found");
    }

    const shop = await this.shopRepository.findFirst();
    const upis = await this.upiRepository.findAll();

    InvoicePdf.generate({
      sale,
      shop,
      upis
    }, res, options);
  }

  async sendWhatsappInvoice(saleId: bigint): Promise<void> {
    const sale = await prisma.sale.findUnique({
      where: { id: saleId },
      include: { customer: { select: { name: true, mobileNo: true } } },
    });

    if (!sale) {
      throw new Error("Sale not found");
    }

    if (sale.isWhatsappMessageSent) {
      throw new Error("WhatsApp message already sent for this sale");
    }

    const phone = sale.customerId
      ? sale.customer?.mobileNo ?? null
      : sale.walkInMobile ?? null;

    if (!phone) {
      throw new Error("No phone number available to send WhatsApp message");
    }

    if (!whatsappService.isConfigured()) {
      throw new Error("WhatsApp service is not configured");
    }

    const customerName = sale.customerId
      ? sale.customer?.name ?? "Customer"
      : sale.walkInName ?? "Customer";

    const invoiceUuid = sale.invoiceDownloadToken;
    const formattedAmount = Number(sale.grandTotal ?? 0).toFixed(2);

    await whatsappService.sendInvoiceReady({
      phone,
      customerName,
      invoiceNo: sale.invoiceNo,
      invoiceAmount: formattedAmount,
      invoiceUuid,
      invoiceUrl: sale.invoiceDownloadToken,
    });

    await prisma.sale.update({
      where: { id: saleId },
      data: { isWhatsappMessageSent: true },
    });
  }
}
