import { BadRequestException, 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 { Item, ItemDocument } from "./schemas/item.schema";
import { StockMovementSheet, StockMovementSheetDocument } from "./schemas/stock-movement-sheet.schema";
import { StockTransaction, StockTransactionDocument } from "./schemas/stock-transaction.schema";

/**
 * Normalise a raw Mongoose item document for API responses.
 * The real collection uses 'description' as the display name and
 * 'bagWeightKg' (number) instead of a unit string.
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function mapItem(item: any) {
  const bagWeightKg: number | null = item.bagWeightKg ?? null;
  // Build unit string from bagWeightKg if unit field is absent
  const unit: string | null =
    item.unit ?? (bagWeightKg ? `BAG/1x${bagWeightKg}kg` : null);

  // itemName: prefer explicit itemName, fall back to description
  const itemName: string | null =
    item.itemName ?? item.description ?? null;

  return {
    ...item,
    itemName,
    unit,
    // Expose _id as string for convenience
    _id: item._id?.toString() ?? item._id,
  };
}

@Injectable()
export class ItemsService {
  constructor(
    @InjectModel(Item.name) private readonly itemModel: Model<ItemDocument>,
    @InjectModel(Batch.name) private readonly batchModel: Model<BatchDocument>,
    @InjectModel(InventoryBalance.name)
    private readonly balanceModel: Model<InventoryBalanceDocument>,
    @InjectModel(StockMovementSheet.name)
    private readonly sheetModel: Model<StockMovementSheetDocument>,
    @InjectModel(StockTransaction.name)
    private readonly stockTxModel: Model<StockTransactionDocument>,
  ) {}

  findAll() {
    return this.itemModel.find().lean().exec().then((items) =>
      items.map((item) => mapItem(item))
    );
  }

  async findByCode(itemCode: string) {
    const item = await this.itemModel.findOne({ itemCode }).lean().exec();
    if (!item) throw new BadRequestException(`Item code '${itemCode}' not found`);
    return mapItem(item);
  }

  async create(payload: {
    itemCode: string;
    itemName?: string;
    description?: string;
    brand?: string;
    barcode?: string;
    countryOfOrigin?: string;
    blend?: string;
    grainType?: string;
    varietyType?: string;
    processType?: string;
    unit?: string;
    bagWeightKg?: number;
    packing?: string;
    variant?: string;
    riceName?: string;
    category?: string;
  }) {
    if (!payload.itemCode) throw new BadRequestException("itemCode is required");

    const existing = await this.itemModel.findOne({ itemCode: payload.itemCode }).exec();
    if (existing) throw new BadRequestException(`Item code '${payload.itemCode}' already exists`);

    const description = payload.description ?? payload.itemName ?? null;
    const itemName = payload.itemName ?? payload.description ?? null;
    const bagWeightKg = payload.bagWeightKg ?? null;
    const unit = payload.unit ?? (bagWeightKg ? `BAG/1x${bagWeightKg}kg` : null);

    const item = await this.itemModel.create({
      itemCode: payload.itemCode,
      itemName,
      description,
      normalizedName: (itemName ?? payload.itemCode).trim().toLowerCase(),
      brand: payload.brand ?? null,
      barcode: payload.barcode ?? null,
      countryOfOrigin: payload.countryOfOrigin ?? null,
      blend: payload.blend ?? null,
      grainType: payload.grainType ?? null,
      varietyType: payload.varietyType ?? null,
      processType: payload.processType ?? null,
      unit,
      bagWeightKg,
      packing: payload.packing ?? null,
      variant: payload.variant ?? null,
      riceName: payload.riceName ?? null,
      category: payload.category ?? null,
    });
    return mapItem(item.toObject());
  }

  async update(
    id: string,
    payload: Partial<{
      itemCode: string;
      itemName: string;
      description: string;
      brand: string;
      barcode: string;
      countryOfOrigin: string;
      blend: string;
      grainType: string;
      varietyType: string;
      processType: string;
      unit: string;
      bagWeightKg: number;
      packing: string;
      variant: string;
      riceName: string;
      category: string;
    }>,
  ) {
    // If bagWeightKg is being set and unit isn't explicitly provided, compute unit
    const update: Record<string, unknown> = { ...payload };
    if (payload.bagWeightKg && !payload.unit) {
      update.unit = `BAG/1x${payload.bagWeightKg}kg`;
    }
    const item = await this.itemModel.findByIdAndUpdate(
      id,
      { $set: update },
      { new: true },
    ).lean().exec();
    if (!item) throw new BadRequestException("Item not found");
    return mapItem(item);
  }

  /**
   * Search items by query string matching itemCode, itemName, or description.
   * Returns up to 10 results. Queries < 2 characters return empty array.
   * Special regex characters are escaped to prevent injection.
   */
  async searchItems(query: string) {
    // Return empty array for queries < 2 characters
    if (!query || query.length < 2) {
      return [];
    }

    // Escape special regex characters to prevent regex injection
    const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    
    // Create case-insensitive regex
    const regex = new RegExp(escapedQuery, 'i');

    // Search across itemCode, itemName, and description fields
    const items = await this.itemModel
      .find({
        $or: [
          { itemCode: regex },
          { itemName: regex },
          { description: regex }
        ]
      })
      .limit(10)
      .lean()
      .exec();

    return items.map((item) => mapItem(item));
  }

  async findBatches(itemId: string, warehouseId: string) {
    const batches = await this.batchModel
      .find({
        itemId: new Types.ObjectId(itemId),
        warehouseId: new Types.ObjectId(warehouseId),
        expiryDate: { $exists: true, $nin: [null, ""] },
      })
      .lean()
      .exec();
    const batchByNumber = new Map(batches.map((batch) => [batch.batchNumber, batch]));

    // Get item to access bagWeightKg
    const item = await this.itemModel.findById(itemId).lean().exec();
    const bagWeightKgFromItem: number = (item as any)?.bagWeightKg ?? 0;

    // Get approved sheets to read real MT
    const approvedSheets = await this.sheetModel
      .find({ approvalStatus: "approved" })
      .lean()
      .exec();

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

    for (const sheet of approvedSheets) {
      for (const li of (sheet.lineItems ?? []) as any[]) {
        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 kgPerBag = bagWeightKgFromItem || (() => {
          const unit = li.unit ?? "";
          const m = unit.match(/(\d+)x(\d+(?:\.\d+)?)kg/i);
          return m ? parseFloat(m[1]) * parseFloat(m[2]) : 0;
        })();
        if (kgPerBag <= 0) continue;

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

        if (existing) {
          existing.availableBags += warehouseBags;
          existing.availableMt += availableMt;
        } else {
          batchExpiryMap.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 = batchExpiryMap.get(key);
      if (!existing) continue;

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

    const results = Array.from(batchExpiryMap.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));

    if (results.length > 0) return results;

    return Promise.all(
      batches.map(async (batch) => {
        const balance = await this.balanceModel.findOne({ batchId: batch._id }).lean().exec();
        const availableMt = balance?.currentQuantityMt ?? 0;
        return {
          id: (batch._id as Types.ObjectId).toString(),
          batchNumber: batch.batchNumber,
          expiryDate: batch.expiryDate,
          availableBags: bagWeightKgFromItem > 0 ? Math.round((availableMt * 1000) / bagWeightKgFromItem) : undefined,
          availableMt: Number(availableMt.toFixed(4)),
        };
      }),
    ).then((fallback) => fallback.filter((batch) => batch.availableMt > 0));
  }
}
