import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model, Types } from "mongoose";
import { Batch, BatchDocument } from "./schemas/batch.schema";
import { InventoryBalance, InventoryBalanceDocument } from "./schemas/inventory-balance.schema";
import { InventoryThresholdState, InventoryThresholdStateDocument } from "./schemas/inventory-threshold-state.schema";
import { InventoryTransaction, InventoryTransactionDocument } from "./schemas/inventory-transaction.schema";
import { Item, ItemDocument } from "./schemas/item.schema";
import { StockMovementSheet, StockMovementSheetDocument } from "./schemas/stock-movement-sheet.schema";
import { StockTransaction, StockTransactionDocument } from "./schemas/stock-transaction.schema";
import { Warehouse, WarehouseDocument } from "./schemas/warehouse.schema";

@Injectable()
export class InventoryService {
  private readonly thresholdTargetMt = 26000;
  private readonly thresholdStateKey = "global-stock-in-invoice";

  constructor(
    @InjectModel(Warehouse.name) private readonly warehouseModel: Model<WarehouseDocument>,
    @InjectModel(Item.name) private readonly itemModel: Model<ItemDocument>,
    @InjectModel(Batch.name) private readonly batchModel: Model<BatchDocument>,
    @InjectModel(InventoryTransaction.name)
    private readonly transactionModel: Model<InventoryTransactionDocument>,
    @InjectModel(InventoryBalance.name)
    private readonly balanceModel: Model<InventoryBalanceDocument>,
    @InjectModel(StockTransaction.name)
    private readonly stockTxModel: Model<StockTransactionDocument>,
    @InjectModel(StockMovementSheet.name)
    private readonly sheetModel: Model<StockMovementSheetDocument>,
    @InjectModel(InventoryThresholdState.name)
    private readonly thresholdStateModel: Model<InventoryThresholdStateDocument>,
  ) {}

  async getSummary() {
    // totalAvailableMt = sum of all inventory_balance records.
    // These are written (and kept up-to-date) by the approve() and
    // stock-out flows, so they are always the ground truth.
    // We no longer read sheet.totalMtAll here — that snapshot can be
    // stale when a sheet was submitted before bagWeightKg was set.
    const balances = await this.balanceModel.find().lean().exec();
    const totalAvailableMt = balances.reduce(
      (sum, b) => sum + (b.currentQuantityMt ?? 0),
      0,
    );

    const now = new Date();
    const limit = new Date(now);
    limit.setMonth(limit.getMonth() + 6);

    // Count unique batches expiring within 6 months — still read from sheets
    // (sheets hold the expiry date + batch metadata)
    const approvedSheets = await this.sheetModel
      .find({ approvalStatus: "approved" })
      .lean()
      .exec();

    // Count unique batches expiring within 6 months from approved sheets
    const batchExpiries = new Set<string>();
    for (const sheet of approvedSheets) {
      for (const li of sheet.lineItems ?? []) {
        if (!li.expiryDate || !li.batchNumber) continue;
        const expiry = new Date(li.expiryDate);
        if (expiry <= limit) {
          batchExpiries.add(`${li.itemCode ?? ""}|${li.batchNumber}`);
        }
      }
    }
    const expiringWithinSixMonths = batchExpiries.size;

    const pendingReviews = await this.stockTxModel
      .countDocuments({ approvalStatus: "pending_approval" })
      .exec();

    const totalEmptyMt = Math.max(0, this.thresholdTargetMt - totalAvailableMt);
    const thresholdBasisMt = await this.getHistoricalThresholdBasis(totalAvailableMt);
    const thresholdState = await this.syncThresholdState(thresholdBasisMt);

    return {
      totalAvailableMt: Number(totalAvailableMt.toFixed(2)),
      totalEmptyMt: Number(totalEmptyMt.toFixed(2)),
      thresholdTargetMt: this.thresholdTargetMt,
      hasReachedThreshold: thresholdState.hasReachedThreshold,
      invoiceRequiredForStockIn: !thresholdState.hasReachedThreshold && totalAvailableMt < this.thresholdTargetMt,
      expiringWithinSixMonths,
      pendingReviews,
    };
  }

  async getInventory() {
    const balances = await this.balanceModel.find().lean().exec();

    const results = await Promise.all(
      balances.map(async (balance) => {
        const warehouse = await this.warehouseModel.findById(balance.warehouseId).lean().exec();
        const item = await this.itemModel.findById(balance.itemId).lean().exec();
        const batch = await this.batchModel.findById(balance.batchId).lean().exec();

        const inbound = await this.sumTransactions(balance.batchId.toString(), "inbound");
        const outbound = await this.sumTransactions(balance.batchId.toString(), "outbound");

        return {
          id: (balance._id as Types.ObjectId).toString(),
          warehouse: warehouse?.name ?? null,
          itemCode: item?.itemCode ?? null,
          itemName: item?.itemName ?? null,
          batchNumber: batch?.batchNumber ?? null,
          expiryDate: batch?.expiryDate ?? null,
          inboundMt: inbound,
          outboundMt: outbound,
          availableMt: balance.currentQuantityMt,
          thresholdTargetMt: balance.thresholdTargetMt,
          thresholdProgressPct: Number(
            ((balance.currentQuantityMt / balance.thresholdTargetMt) * 100).toFixed(2),
          ),
        };
      }),
    );

    return results;
  }

  async getExpiry(months = 6, warehouseNames?: string[]) {
    const now = new Date();
    const limit = new Date(now);
    limit.setMonth(limit.getMonth() + months);

    // Pull expiry data from approved strategic sheet line items
    // (this is where actual batch + expiry data lives)
    const approvedSheets = await this.sheetModel
      .find({ approvalStatus: "approved" })
      .lean()
      .exec();

    // Build a map: key = "itemCode|batchNumber" → aggregate MT across warehouses
    const batchMap = new Map<string, {
      itemCode: string | null;
      itemName: string | null;
      batchNumber: string;
      expiryDate: string | null;
      warehouses: Set<string>;
      totalMt: number;
    }>();

    // Also collect warehouse names for filtering
    const warehouseIdToName = new Map<string, string>();
    const allWarehouses = await this.warehouseModel.find().lean().exec();
    allWarehouses.forEach((w) => {
      warehouseIdToName.set((w._id as Types.ObjectId).toString(), w.name);
    });

    // Build a set of stocked-out batch keys (itemCode|batchNumber) to exclude
    const stockedOutBatches = await this.batchModel
      .find({ stockedOut: true })
      .lean()
      .exec();
    const stockedOutKeys = new Set(
      stockedOutBatches.map((b) => `${b.itemId?.toString()}|${b.batchNumber}`)
    );

    for (const sheet of approvedSheets) {
      for (const li of sheet.lineItems ?? []) {
        if (!li.batchNumber || !li.expiryDate) continue;

        // Skip batches that have been stocked out
        const stockedOutKey = `${li.itemId?.toString()}|${li.batchNumber}`;
        if (stockedOutKeys.has(stockedOutKey)) continue;

        const batchKey = `${li.itemCode ?? ""}|${li.batchNumber}`;
        const existing = batchMap.get(batchKey);

        // Collect warehouse names for this line item
        const whNames = new Set<string>();
        for (const whId of Object.keys(li.warehouseBags ?? {})) {
          const name = warehouseIdToName.get(whId) ?? whId;
          whNames.add(name);
        }

        if (existing) {
          existing.totalMt += li.totalMt ?? 0;
          whNames.forEach((n) => existing.warehouses.add(n));
          if (li.expiryDate && (!existing.expiryDate || li.expiryDate > existing.expiryDate)) {
            existing.expiryDate = li.expiryDate;
          }
        } else {
          batchMap.set(batchKey, {
            itemCode: li.itemCode ?? null,
            itemName: li.itemName ?? null,
            batchNumber: li.batchNumber,
            expiryDate: li.expiryDate,
            warehouses: whNames,
            totalMt: li.totalMt ?? 0,
          });
        }
      }
    }

    // Convert map to rows and apply filters
    const rows = Array.from(batchMap.values()).map((entry) => ({
      warehouse: entry.warehouses.size > 0 ? Array.from(entry.warehouses).join(", ") : null,
      itemCode: entry.itemCode,
      itemName: entry.itemName,
      batchNumber: entry.batchNumber,
      expiryDate: entry.expiryDate,
      availableMt: Number(entry.totalMt.toFixed(4)),
    }));

    return rows
      .filter((row) => {
        if (!row.expiryDate) return false;
        const expiry = new Date(row.expiryDate);
        // For months=0 show only already expired; otherwise show up to limit
        if (months === 0) {
          if (expiry >= now) return false;
        } else {
          if (expiry > limit) return false;
        }
        if (warehouseNames && warehouseNames.length > 0) {
          if (!row.warehouse) return false;
          return warehouseNames.some((wn) =>
            row.warehouse!.toLowerCase().includes(wn.toLowerCase())
          );
        }
        return true;
      })
      .sort((a, b) => String(a.expiryDate).localeCompare(String(b.expiryDate)));
  }

  async getBatches(itemId: string, warehouseId: string) {
    // Batch numbers are not unique by expiry in older data, so build stock-out
    // choices from approved sheet line items and keep each batch+expiry slice separate.
    const batches = await this.batchModel
      .find({
        itemId: new Types.ObjectId(itemId),
        warehouseId: new Types.ObjectId(warehouseId),
      })
      .lean()
      .exec();
    const batchByNumber = new Map(batches.map((batch) => [batch.batchNumber, batch]));

    const approvedSheets = await this.sheetModel
      .find({ approvalStatus: "approved" })
      .lean()
      .exec();

    const item = await this.itemModel.findById(itemId).lean().exec();
    const bagWeightKg = item?.bagWeightKg ?? 25;

    const batchMap = new Map<string, {
      id: string;
      batchNumber: string;
      expiryDate: string;
      availableBags: number;
      availableMt: number;
    }>();

    for (const sheet of approvedSheets) {
      for (const li of sheet.lineItems ?? []) {
        if (li.itemId?.toString() !== itemId || !li.batchNumber || !li.expiryDate) continue;

        const warehouseBags = (li.warehouseBags ?? {})[warehouseId] ?? 0;
        if (warehouseBags <= 0) continue;

        const batch = batchByNumber.get(li.batchNumber);
        if (!batch) continue;

        const key = `${li.batchNumber}|${li.expiryDate}`;
        const availableMt = (warehouseBags * bagWeightKg) / 1000;
        const existing = batchMap.get(key);

        if (existing) {
          existing.availableBags += warehouseBags;
          existing.availableMt += availableMt;
        } else {
          batchMap.set(key, {
            id: (batch._id as Types.ObjectId).toString(),
            batchNumber: li.batchNumber,
            expiryDate: li.expiryDate,
            availableBags: warehouseBags,
            availableMt,
          });
        }
      }
    }

    const approvedStockOuts = await this.stockTxModel
      .find({
        type: "outbound",
        approvalStatus: "approved",
        warehouseId: new Types.ObjectId(warehouseId),
        itemId: new Types.ObjectId(itemId),
      })
      .lean()
      .exec();

    for (const tx of approvedStockOuts) {
      const batch = batches.find((b) => b._id.toString() === tx.batchId?.toString());
      if (!batch) continue;
      const expiryDate = tx.expiryDate ?? batch.expiryDate;
      const key = `${batch.batchNumber}|${expiryDate}`;
      const existing = batchMap.get(key);
      if (!existing) continue;

      const deductedBags = Math.round(((tx.quantityMt ?? 0) * 1000) / bagWeightKg);
      existing.availableBags = Math.max(0, existing.availableBags - deductedBags);
      existing.availableMt = Math.max(0, existing.availableMt - (tx.quantityMt ?? 0));
    }

    return Array.from(batchMap.values())
      .filter((batch) => batch.availableBags > 0 && batch.availableMt > 0)
      .map((batch) => ({
        ...batch,
        availableMt: Number(batch.availableMt.toFixed(4)),
      }))
      .sort((a, b) => a.batchNumber.localeCompare(b.batchNumber) || a.expiryDate.localeCompare(b.expiryDate));
  }

  private async sumTransactions(batchId: string, type: "inbound" | "outbound") {
    const result = await this.transactionModel.aggregate([
      { $match: { batchId: new Types.ObjectId(batchId), type } },
      { $group: { _id: null, total: { $sum: "$quantityMt" } } },
    ]);
    return result[0]?.total ?? 0;
  }

  private async syncThresholdState(totalAvailableMt: number) {
    const now = new Date().toISOString();
    let state = await this.thresholdStateModel.findOne({ key: this.thresholdStateKey }).exec();

    if (!state) {
      state = await this.thresholdStateModel.create({
        key: this.thresholdStateKey,
        thresholdTargetMt: this.thresholdTargetMt,
        hasReachedThreshold: totalAvailableMt >= this.thresholdTargetMt,
        reachedAt: totalAvailableMt >= this.thresholdTargetMt ? now : null,
        updatedAt: now,
      });
      return state;
    }

    if (!state.hasReachedThreshold && totalAvailableMt >= this.thresholdTargetMt) {
      state.hasReachedThreshold = true;
      state.reachedAt = now;
      state.updatedAt = now;
      await state.save();
    }

    return state;
  }

  private async getHistoricalThresholdBasis(currentTotalMt: number) {
    const outbound = await this.stockTxModel.aggregate([
      { $match: { type: "outbound", approvalStatus: "approved" } },
      { $group: { _id: null, total: { $sum: "$quantityMt" } } },
    ]);
    const approvedOutboundMt = outbound[0]?.total ?? 0;
    return currentTotalMt + approvedOutboundMt;
  }
}
