import prisma from "../config/prisma.js";
import { Prisma } from "../generated/prisma/client.js";
import { ArrivalRepository } from "../repositories/ArrivalRepository.js";

export class ArrivalService {
  constructor(private readonly arrivalRepository = new ArrivalRepository()) {}

  async checkLotAvailability(lotNo: string, vegetableId?: bigint, excludeId?: bigint) {
    const activeLot = await this.arrivalRepository.findActiveByLotNo(lotNo, excludeId, vegetableId);

    if (activeLot) {
      const vegName = activeLot.vegetable?.name || "another item";
      const remainingQty = Number(activeLot.remainingQuantity || 0);
      const remainingBags = Number(activeLot.remainingBags || 0);
      const isFlexible = (activeLot.vegetable as any)?.isFlexible;

      let message = "";
      if (isFlexible) {
        message = `Lot "${lotNo}" is already active for ${vegName}. The lot must be closed before reuse.`;
      } else {
        let stockInfo = "";
        if (remainingQty > 0 && remainingBags > 0) {
          stockInfo = ` (${remainingQty} KG / ${remainingBags} ${activeLot.unit || 'BAG'} remaining)`;
        } else if (remainingBags > 0) {
          stockInfo = ` (${remainingBags} ${activeLot.unit || 'BAG'} remaining)`;
        } else if (remainingQty > 0) {
          stockInfo = ` (${remainingQty} KG remaining)`;
        }
        message = `Lot "${lotNo}" already has active stock for ${vegName}${stockInfo}. It will be reusable once fully sold.`;
      }

      return {
        isAvailable: false,
        activeLotId: activeLot.id.toString(),
        vegetableName: vegName,
        remainingQuantity: remainingQty,
        remainingBags: remainingBags,
        status: activeLot.status,
        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.arrivalRepository.findAnyByLotNo(lotNo);
    if (vegetableId && previousUse && previousUse.vegetableId !== vegetableId) {
      return {
        isAvailable: false,
        activeLotId: null,
        remainingQuantity: 0,
        remainingBags: 0,
        status: "UNAVAILABLE",
        message: `Lot "${lotNo}" is permanently assigned to a different vegetable and cannot be assigned to another.`,
      };
    }
    */

    return {
      isAvailable: true,
      activeLotId: null,
      vegetableName: null,
      remainingQuantity: 0,
      remainingBags: 0,
      status: "AVAILABLE",
      message: `Lot "${lotNo}" is available for entry.`,
    };
  }

  async createArrival(data: any, userId?: bigint) {
    const vegetableId = BigInt(data.vegetableId);
    const supplierId = data.supplierId ? BigInt(data.supplierId) : null;
    const lotNo = String(data.lotNo).trim();

    // 1. Verify Lot Reusability
    const availability = await this.checkLotAvailability(lotNo, vegetableId);
    if (!availability.isAvailable) {
      throw new Error(availability.message);
    }

    // 2. Quantity & Bag Calculations
    let quantity = new Prisma.Decimal(data.quantity || 0);
    let bags = new Prisma.Decimal(data.bags || 0);
    let weightPerBag = data.weightPerBag ? new Prisma.Decimal(data.weightPerBag) : null;

    if (data.unit !== "KG") {
      if (!data.bags || Number(data.bags) <= 0) {
        const unitName = data.unit === "BOX" ? "Boxes" : (data.unit === "THAAR" ? "Thaar" : (data.unit === "PIECES" ? "Pieces" : "Bags"));
        throw new Error(`Number of ${unitName} is required.`);
      }
      if (quantity.gt(0)) {
        weightPerBag = quantity.div(bags).toDecimalPlaces(3, Prisma.Decimal.ROUND_HALF_UP);
      }
    } else {
      if (!data.quantity || Number(data.quantity) <= 0) {
        throw new Error("Quantity in KG is required.");
      }
      if (bags.gt(0) && quantity.gt(0)) {
        weightPerBag = quantity.div(bags).toDecimalPlaces(3, Prisma.Decimal.ROUND_HALF_UP);
      }
    }

    // 3. Freight & Logistics Expenses
    const freight = new Prisma.Decimal(data.freight || 0);
    const extraFreight = new Prisma.Decimal(data.extraFreight || 0);
    const associationFee = new Prisma.Decimal(data.associationFee || 0);
    const unloadingFee = new Prisma.Decimal(data.unloadingFee || 0);
    const miscellaneousFee = new Prisma.Decimal(data.miscellaneousFee || 0);
    const totalFreight = freight
      .plus(extraFreight)
      .plus(associationFee)
      .plus(unloadingFee)
      .plus(miscellaneousFee);

    const advanceAmount = new Prisma.Decimal(data.advanceAmount || 0);
    const arrivalDate = data.arrivalDate ? new Date(data.arrivalDate) : new Date();
    const dateOfDespatch = data.dateOfDespatch ? new Date(data.dateOfDespatch) : null;

    if (dateOfDespatch && dateOfDespatch > arrivalDate) {
      throw new Error("Dispatched date cannot be later than the arrival date.");
    }

    return await prisma.$transaction(async (tx) => {
      const arrival = await this.arrivalRepository.create(
        {
          purchaseType: "COMMISSION_ARRIVAL",
          purchaseDate: arrivalDate,
          arrivalDate,
          dateOfDespatch,
          billNo: data.billNo || null,
          supplierId,
          cashSupplierName: data.cashSupplierName || null,
          vegetableId,
          lotNo,
          status: "ACTIVE",
          unit: data.unit || "KG",
          rateType: quantity.gt(0) ? "KG" : "UNIT",
          bags,
          weightPerBag,
          quantity,
          ratePerUnit: new Prisma.Decimal(0),
          freight,
          extraFreight,
          associationFee,
          unloadingFee,
          miscellaneousFee,
          totalFreight,
          goodsTotal: new Prisma.Decimal(0),
          surchargeTotal: totalFreight,
          costPerUnit: new Prisma.Decimal(0),
          grandTotal: totalFreight,
          advanceAmount,
          balanceAmount: new Prisma.Decimal(0),
          remainingQuantity: quantity,
          remainingBags: bags,
          placeOfDespatch: data.placeOfDespatch || null,
          placeOfArrival: data.placeOfArrival || null,
          truckNo: data.truckNo || null,
          agentName: data.agentName || null,
          remarks: data.remarks || null,
          createdBy: userId || null,
        },
        tx
      );

      return arrival;
    });
  }

  async getAllArrivals(query: {
    page?: number;
    limit?: number;
    search?: string;
    dateFrom?: string;
    dateTo?: string;
    supplierId?: string;
    vegetableId?: string;
    settlementStatus?: "UNSETTLED" | "PARTIALLY_SETTLED" | "SETTLED" | "ALL";
  }) {
    const page = Number(query.page) || 1;
    const limit = Number(query.limit) || 20;
    const skip = (page - 1) * limit;

    const dateFrom = query.dateFrom ? new Date(query.dateFrom) : undefined;
    const dateTo = query.dateTo ? new Date(query.dateTo) : undefined;
    const supplierId = query.supplierId ? BigInt(query.supplierId) : undefined;
    const vegetableId = query.vegetableId ? BigInt(query.vegetableId) : undefined;

    const { items, total } = await this.arrivalRepository.findAll(
      skip,
      limit,
      query.search,
      dateFrom,
      dateTo,
      supplierId,
      vegetableId,
      query.settlementStatus
    );

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

  async getArrivalById(id: bigint) {
    const arrival = await this.arrivalRepository.findById(id);
    if (!arrival) {
      throw new Error("Arrival lot not found.");
    }
    const salesSummary = await this.arrivalRepository.getArrivalSalesSummary(id);
    // Override top-level saleItems (wholesale-only from Prisma include) with
    // the combined wholesale + retail list so the view modal shows both channels.
    return { ...arrival, salesSummary, saleItems: salesSummary.saleItems };
  }

  async updateArrival(id: bigint, data: any) {
    const existing = await this.arrivalRepository.findById(id);
    if (!existing) {
      throw new Error("Arrival lot not found.");
    }

    if (existing.arrivalSettlement && existing.arrivalSettlement.status === "SETTLED") {
      throw new Error("Cannot modify an arrival lot that is already SETTLED in P.Patti.");
    }

    const vegetableId = data.vegetableId ? BigInt(data.vegetableId) : existing.vegetableId;
    const lotNo = data.lotNo ? String(data.lotNo).trim() : existing.lotNo;

    if (lotNo !== existing.lotNo || vegetableId !== existing.vegetableId) {
      const availability = await this.checkLotAvailability(lotNo, vegetableId, id);
      if (!availability.isAvailable) {
        throw new Error(availability.message);
      }
    }

    const freight = data.freight !== undefined ? new Prisma.Decimal(data.freight) : existing.freight;
    const extraFreight = data.extraFreight !== undefined ? new Prisma.Decimal(data.extraFreight) : existing.extraFreight;
    const associationFee = data.associationFee !== undefined ? new Prisma.Decimal(data.associationFee) : existing.associationFee;
    const unloadingFee = data.unloadingFee !== undefined ? new Prisma.Decimal(data.unloadingFee) : existing.unloadingFee;
    const miscellaneousFee = data.miscellaneousFee !== undefined ? new Prisma.Decimal(data.miscellaneousFee) : existing.miscellaneousFee;
    const totalFreight = freight
      .plus(extraFreight)
      .plus(associationFee)
      .plus(unloadingFee)
      .plus(miscellaneousFee);

    const arrivalDate = data.arrivalDate ? new Date(data.arrivalDate) : existing.arrivalDate;
    const dateOfDespatch = data.dateOfDespatch ? new Date(data.dateOfDespatch) : existing.dateOfDespatch;

    if (dateOfDespatch && arrivalDate && dateOfDespatch > arrivalDate) {
      throw new Error("Dispatched date cannot be later than the arrival date.");
    }

    let newQuantity = existing.quantity;
    let newRemainingQuantity = existing.remainingQuantity;
    if (data.quantity !== undefined) {
      const parsedQty = new Prisma.Decimal(data.quantity);
      const diff = parsedQty.minus(existing.quantity);
      newRemainingQuantity = existing.remainingQuantity.plus(diff);
      if (newRemainingQuantity.lessThan(0)) {
        throw new Error("Cannot reduce quantity below the amount already sold.");
      }
      newQuantity = parsedQty;
    }

    let newBags = existing.bags;
    let newRemainingBags = existing.remainingBags;
    if (data.unit && data.unit !== 'KG' && data.bags !== undefined) {
      const parsedBags = new Prisma.Decimal(data.bags);
      const oldBags = existing.bags || new Prisma.Decimal(0);
      const diff = parsedBags.minus(oldBags);
      newRemainingBags = (existing.remainingBags || new Prisma.Decimal(0)).plus(diff);
      if (newRemainingBags.lessThan(0)) {
        throw new Error("Cannot reduce bags below the amount already sold.");
      }
      newBags = parsedBags;
    } else if (data.unit === 'KG') {
      newBags = new Prisma.Decimal(0);
      newRemainingBags = new Prisma.Decimal(0);
    } else if (data.bags !== undefined) {
      // In case unit isn't passed but bags is (partial update)
      const parsedBags = new Prisma.Decimal(data.bags);
      const oldBags = existing.bags || new Prisma.Decimal(0);
      const diff = parsedBags.minus(oldBags);
      newRemainingBags = (existing.remainingBags || new Prisma.Decimal(0)).plus(diff);
      if (newRemainingBags.lessThan(0)) {
        throw new Error("Cannot reduce bags below the amount already sold.");
      }
      newBags = parsedBags;
    }

    return await this.arrivalRepository.update(id, {
      supplierId: data.supplierId ? BigInt(data.supplierId) : existing.supplierId,
      cashSupplierName: data.cashSupplierName !== undefined ? data.cashSupplierName : existing.cashSupplierName,
      vegetableId,
      lotNo,
      billNo: data.billNo !== undefined ? data.billNo : existing.billNo,
      unit: data.unit !== undefined ? data.unit : existing.unit,
      quantity: newQuantity,
      remainingQuantity: newRemainingQuantity,
      bags: newBags,
      remainingBags: newRemainingBags,
      dateOfDespatch: data.dateOfDespatch ? new Date(data.dateOfDespatch) : existing.dateOfDespatch,
      // Keep purchaseDate in sync with arrivalDate so date-filtered reports remain accurate
      arrivalDate: data.arrivalDate ? new Date(data.arrivalDate) : existing.arrivalDate,
      purchaseDate: data.arrivalDate ? new Date(data.arrivalDate) : existing.purchaseDate,
      placeOfDespatch: data.placeOfDespatch !== undefined ? data.placeOfDespatch : existing.placeOfDespatch,
      placeOfArrival: data.placeOfArrival !== undefined ? data.placeOfArrival : existing.placeOfArrival,
      truckNo: data.truckNo !== undefined ? data.truckNo : existing.truckNo,
      agentName: data.agentName !== undefined ? data.agentName : existing.agentName,
      freight,
      extraFreight,
      associationFee,
      unloadingFee,
      miscellaneousFee,
      totalFreight,
      surchargeTotal: totalFreight,
      grandTotal: totalFreight,
      advanceAmount: data.advanceAmount !== undefined ? new Prisma.Decimal(data.advanceAmount) : existing.advanceAmount,
      remarks: data.remarks !== undefined ? data.remarks : existing.remarks,
    });
  }

  async deleteArrival(id: bigint) {
    const existing = await this.arrivalRepository.findById(id);
    if (!existing) {
      throw new Error("Arrival lot not found.");
    }

    if (existing.saleItems && existing.saleItems.length > 0) {
      throw new Error("Cannot delete arrival lot because sales bills have already been recorded against it.");
    }

    if (existing.arrivalSettlement) {
      throw new Error("Cannot delete arrival lot with existing P.Patti settlement.");
    }

    // Prevent delete if retail stock has been allocated — those rows would become orphaned
    if (existing.retailStocks && existing.retailStocks.length > 0) {
      throw new Error("Cannot delete arrival lot because retail stock has already been allocated from it.");
    }

    return await this.arrivalRepository.softDelete(id);
  }
}
