import { BadRequestException, Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model, Types } from "mongoose";
import { NotificationsService } from "./notifications.service";
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 { Item, ItemDocument } from "./schemas/item.schema";
import { StockMovementSheet, StockMovementSheetDocument } from "./schemas/stock-movement-sheet.schema";
import { StockTransaction, StockTransactionDocument } from "./schemas/stock-transaction.schema";
import { User, UserDocument } from "./schemas/user.schema";
import { Warehouse, WarehouseDocument } from "./schemas/warehouse.schema";

const GLOBAL_MT_THRESHOLD = 26000;
const THRESHOLD_STATE_KEY = "global-stock-in-invoice";

@Injectable()
export class StockTransactionsService {
  constructor(
    @InjectModel(StockTransaction.name)
    private readonly txModel: Model<StockTransactionDocument>,
    @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(InventoryBalance.name)
    private readonly balanceModel: Model<InventoryBalanceDocument>,
    @InjectModel(StockMovementSheet.name)
    private readonly sheetModel: Model<StockMovementSheetDocument>,
    @InjectModel(User.name)
    private readonly userModel: Model<UserDocument>,
    @InjectModel(InventoryThresholdState.name)
    private readonly thresholdStateModel: Model<InventoryThresholdStateDocument>,
    private readonly notificationsService: NotificationsService,
  ) {}

  // ── Stock In ──────────────────────────────────────────────────────────────

  async createStockIn(payload: {
    warehouseId: string;
    itemId: string;
    batchNumber: string;
    expiryDate: string;
    quantityMt: number;
    submittedBy?: string;
    supplierName?: string;
    supplierTrn?: string;
    documentNumber?: string;
    documentDate?: string;
    referenceNumber?: string;
    unit?: string;
    unitPrice?: number;
  }) {
    if (!payload.warehouseId) throw new BadRequestException("warehouseId is required");
    if (!payload.itemId) throw new BadRequestException("itemId is required");
    if (!payload.batchNumber) throw new BadRequestException("batchNumber is required");
    if (!payload.expiryDate) throw new BadRequestException("expiryDate is required");
    if (!payload.quantityMt || payload.quantityMt <= 0)
      throw new BadRequestException("quantityMt must be greater than 0");

    // Threshold check
    const balances = await this.balanceModel.find().lean().exec();
    const totalAvailable = balances.reduce((s, b) => s + b.currentQuantityMt, 0);
    const projected = totalAvailable + payload.quantityMt;
    if (projected > GLOBAL_MT_THRESHOLD) {
      throw new BadRequestException(
        `Stock in would exceed the global ${GLOBAL_MT_THRESHOLD} MT threshold. ` +
          `Current: ${totalAvailable.toFixed(2)} MT, Adding: ${payload.quantityMt.toFixed(2)} MT, ` +
          `Projected: ${projected.toFixed(2)} MT.`,
      );
    }

    // Find or create batch
    const batch = await this._findOrCreateBatch(
      payload.warehouseId,
      payload.itemId,
      payload.batchNumber,
      payload.expiryDate,
    );

    const tx = await this.txModel.create({
      type: "inbound",
      warehouseId: new Types.ObjectId(payload.warehouseId),
      itemId: new Types.ObjectId(payload.itemId),
      batchId: batch._id,
      quantityMt: payload.quantityMt,
      transactionDate: new Date().toISOString().slice(0, 10),
      approvalStatus: "pending_approval",
      submittedBy: payload.submittedBy ?? null,
      supplierName: payload.supplierName ?? null,
      supplierTrn: payload.supplierTrn ?? null,
      documentNumber: payload.documentNumber ?? null,
      documentDate: payload.documentDate ?? null,
      referenceNumber: payload.referenceNumber ?? null,
      unit: payload.unit ?? null,
      unitPrice: payload.unitPrice ?? null,
      createdAt: new Date().toISOString(),
    });

    // Notify managers
    await this.notificationsService.notifyManagers(
      `New stock-in transaction of ${payload.quantityMt} MT is pending your approval.`,
      (tx._id as Types.ObjectId).toString(),
    );

    return this._mapTx(tx.toObject());
  }

  // ── Stock Out ─────────────────────────────────────────────────────────────

  async createStockOut(payload: {
    warehouseId: string;
    itemId: string;
    batchId?: string; // Optional - not tracking batches for stock-out
    expiryDate?: string;
    quantityMt: number;
    submittedBy?: string;
  }) {
    if (!payload.warehouseId) throw new BadRequestException("warehouseId is required");
    if (!payload.itemId) throw new BadRequestException("itemId is required");
    // Allow 0 or any positive quantity - quantity is optional for stock-out records
    if (payload.quantityMt < 0)
      throw new BadRequestException("quantityMt cannot be negative");

    // Note: balance check skipped — actual available MT is tracked in approved sheets
    // batchId is optional for stock-out - use a placeholder if not provided
    const batchId = payload.batchId && payload.batchId !== "N/A" 
      ? new Types.ObjectId(payload.batchId)
      : new Types.ObjectId("000000000000000000000000"); // Placeholder ObjectId

    // Auto-generate Job Card Reference (JOB-YYYYMMDD-XXXXX format)
    const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
    const existingTransactions = await this.txModel.countDocuments({
      type: "outbound",
      createdAt: { $gte: new Date().toISOString().slice(0, 10) }
    });
    const sequenceNumber = String(existingTransactions + 1).padStart(5, '0');
    const jobCardReference = `JOB-${today}-${sequenceNumber}`;

    const tx = await this.txModel.create({
      type: "outbound",
      warehouseId: new Types.ObjectId(payload.warehouseId),
      itemId: new Types.ObjectId(payload.itemId),
      batchId,
      quantityMt: payload.quantityMt || 0, // Allow 0 quantity
      transactionDate: new Date().toISOString().slice(0, 10),
      approvalStatus: "pending_approval",
      submittedBy: payload.submittedBy ?? null,
      jobCardReference, // Auto-generated job card reference
      expiryDate: payload.expiryDate ?? null,
      createdAt: new Date().toISOString(),
    });

    await this.notificationsService.notifyManagers(
      `New stock-out transaction of ${payload.quantityMt || 0} MT is pending your approval.`,
      (tx._id as Types.ObjectId).toString(),
    );

    return this._mapTx(tx.toObject());
  }

  // ── Pending approvals ─────────────────────────────────────────────────────

  async findPending() {
    const txs = await this.txModel
      .find({ approvalStatus: "pending_approval" })
      .sort({ createdAt: -1 })
      .lean()
      .exec();
    return Promise.all(txs.map((t) => this._mapTxWithDetails(t)));
  }

  async findAll(params?: { status?: string; page?: number; limit?: number }) {
    const query: any = {};
    
    // Apply status filter if provided
    if (params?.status) {
      query.approvalStatus = params.status;
    }
    
    // Build the query
    let queryBuilder = this.txModel.find(query).sort({ createdAt: -1 });
    
    // Apply pagination if provided
    if (params?.page && params?.limit) {
      const skip = (params.page - 1) * params.limit;
      queryBuilder = queryBuilder.skip(skip).limit(params.limit);
    }
    
    const txs = await queryBuilder.lean().exec();
    return Promise.all(txs.map((t: StockTransactionDocument) => this._mapTxWithDetails(t)));
  }

  // ── Approve ───────────────────────────────────────────────────────────────

  async approve(id: string, approvedByName: string) {
    const tx = await this.txModel.findById(id).exec();
    if (!tx) throw new BadRequestException("Transaction not found");
    if (tx.approvalStatus !== "pending_approval")
      throw new BadRequestException("Transaction is not pending approval");

    // Apply inventory change
    await this._applyInventoryChange(
      tx.warehouseId.toString(),
      tx.itemId.toString(),
      tx.batchId.toString(),
      tx.quantityMt,
      tx.type as "inbound" | "outbound",
      tx.expiryDate ?? null,
    );
    if (tx.type === "inbound") {
      await this._markThresholdReachedIfNeeded();
    }

    tx.approvalStatus = "approved";
    tx.approvedBy = approvedByName;
    tx.approvedAt = new Date().toISOString();
    await tx.save();

    // Notify store keepers
    await this._notifyKeepers(
      "approved",
      `Stock transaction of ${tx.quantityMt} MT has been approved by ${approvedByName}.`,
      id,
    );

    return this._mapTx(tx.toObject());
  }

  // ── Reject ────────────────────────────────────────────────────────────────

  async reject(id: string, rejectedByName: string, reason: string) {
    const tx = await this.txModel.findById(id).exec();
    if (!tx) throw new BadRequestException("Transaction not found");
    if (tx.approvalStatus !== "pending_approval")
      throw new BadRequestException("Transaction is not pending approval");

    tx.approvalStatus = "rejected";
    tx.rejectedBy = rejectedByName;
    tx.rejectedAt = new Date().toISOString();
    tx.rejectedReason = reason ?? "";
    await tx.save();

    await this._notifyKeepers(
      "rejected",
      `Stock transaction of ${tx.quantityMt} MT was rejected by ${rejectedByName}. Reason: ${reason || "No reason provided"}.`,
      id,
    );

    return this._mapTx(tx.toObject());
  }

  // ── Private helpers ───────────────────────────────────────────────────────

  private async _findOrCreateBatch(
    warehouseId: string,
    itemId: string,
    batchNumber: string,
    expiryDate: string,
  ) {
    const existing = await this.batchModel
      .findOne({
        warehouseId: new Types.ObjectId(warehouseId),
        itemId: new Types.ObjectId(itemId),
        batchNumber,
      })
      .exec();
    if (existing) {
      existing.expiryDate = expiryDate;
      await existing.save();
      return existing;
    }
    return this.batchModel.create({
      warehouseId: new Types.ObjectId(warehouseId),
      itemId: new Types.ObjectId(itemId),
      batchNumber,
      expiryDate,
    });
  }

  private async _applyInventoryChange(
    warehouseId: string,
    itemId: string,
    batchId: string,
    quantityMt: number,
    type: "inbound" | "outbound",
    expiryDate: string | null,
  ) {
    // Update inventory_balances (legacy — kept for compatibility)
    let balance = await this.balanceModel
      .findOne({ batchId: new Types.ObjectId(batchId) })
      .exec();
    if (!balance) {
      balance = await this.balanceModel.create({
        warehouseId: new Types.ObjectId(warehouseId),
        itemId: new Types.ObjectId(itemId),
        batchId: new Types.ObjectId(batchId),
        currentQuantityMt: 0,
        thresholdTargetMt: GLOBAL_MT_THRESHOLD,
        lastUpdated: new Date().toISOString(),
      });
    }
    
    // For outbound transactions, verify balance exists and has sufficient quantity
    if (type === "outbound") {
      if (!balance) {
        throw new BadRequestException(
          `No inventory balance found for batch ${batchId}. Cannot stock out.`
        );
      }
      if (balance.currentQuantityMt < quantityMt) {
        throw new BadRequestException(
          `Insufficient inventory. Available: ${balance.currentQuantityMt} MT, Requested: ${quantityMt} MT`
        );
      }
    }
    
    const delta = type === "inbound" ? quantityMt : -quantityMt;
    balance.currentQuantityMt = Math.max(0, balance.currentQuantityMt + delta);
    balance.lastUpdated = new Date().toISOString();
    await balance.save();

    // For outbound: mark batch as stocked out ONLY if balance reaches 0
    if (type === "outbound" && balance.currentQuantityMt === 0) {
      const batch = await this.batchModel.findById(batchId).exec();
      if (batch) {
        batch.stockedOut = true;
        await batch.save();
      }
    }

    // For outbound: reduce totalMtAll on the matching approved sheet
    // This is the source of truth for getSummary() totalAvailableMt
    if (type === "outbound") {
      // Find the batch to get batchNumber
      const batch = await this.batchModel.findById(batchId).exec();
      if (batch) {
        // Find approved sheets that have this item+batch+warehouse and reduce MT
        const sheets = await this.sheetModel
          .find({ approvalStatus: "approved" })
          .exec();

        for (const sheet of sheets) {
          let sheetModified = false;
          for (const li of sheet.lineItems as any[]) {
            if (
              li.itemId?.toString() !== itemId ||
              li.batchNumber !== batch.batchNumber ||
              (expiryDate && li.expiryDate !== expiryDate)
            ) continue;

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

            const reduction = Math.min(quantityMt, li.totalMt ?? 0);
            li.totalMt = Math.max(0, (li.totalMt ?? 0) - reduction);
            sheetModified = true;
            break;
          }

          if (sheetModified) {
            const newTotal = (sheet.lineItems as any[]).reduce(
              (s: number, li: any) => s + (li.totalMt ?? 0), 0
            );
            sheet.totalMtAll = Number(newTotal.toFixed(4));
            await sheet.save();
            break;
          }
        }
      }
    }
  }

  private async _notifyKeepers(type: string, message: string, txId: string) {
    const keepers = await this.userModel
      .find({ role: "store_keeper", active: true })
      .lean()
      .exec();
    await Promise.all(
      keepers.map((k) =>
        this.notificationsService.notifyUser(
          (k._id as Types.ObjectId).toString(),
          type,
          message,
          txId,
        ),
      ),
    );
  }

  private async _markThresholdReachedIfNeeded() {
    const balances = await this.balanceModel.find().lean().exec();
    const totalAvailableMt = balances.reduce((sum, b) => sum + (b.currentQuantityMt ?? 0), 0);
    if (totalAvailableMt < GLOBAL_MT_THRESHOLD) return;

    const now = new Date().toISOString();
    const state = await this.thresholdStateModel.findOne({ key: THRESHOLD_STATE_KEY }).exec();
    if (state?.hasReachedThreshold) return;

    if (state) {
      state.thresholdTargetMt = GLOBAL_MT_THRESHOLD;
      state.hasReachedThreshold = true;
      state.reachedAt = now;
      state.updatedAt = now;
      await state.save();
      return;
    }

    await this.thresholdStateModel.create({
      key: THRESHOLD_STATE_KEY,
      thresholdTargetMt: GLOBAL_MT_THRESHOLD,
      hasReachedThreshold: true,
      reachedAt: now,
      updatedAt: now,
    });
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private _mapTx(tx: any) {
    return {
      id: (tx._id as Types.ObjectId)?.toString() ?? tx.id,
      type: tx.type,
      warehouseId: tx.warehouseId?.toString(),
      itemId: tx.itemId?.toString(),
      batchId: tx.batchId?.toString(),
      quantityMt: tx.quantityMt,
      transactionDate: tx.transactionDate,
      approvalStatus: tx.approvalStatus,
      approvedBy: tx.approvedBy ?? null,
      approvedAt: tx.approvedAt ?? null,
      rejectedBy: tx.rejectedBy ?? null,
      rejectedAt: tx.rejectedAt ?? null,
      rejectedReason: tx.rejectedReason ?? null,
      submittedBy: tx.submittedBy ?? null,
      supplierName: tx.supplierName ?? null,
      supplierTrn: tx.supplierTrn ?? null,
      documentNumber: tx.documentNumber ?? null,
      documentDate: tx.documentDate ?? null,
      referenceNumber: tx.referenceNumber ?? null,
      unit: tx.unit ?? null,
      unitPrice: tx.unitPrice ?? null,
      jobCardReference: tx.jobCardReference ?? null, // Include job card reference
      expiryDate: tx.expiryDate ?? null,
      createdAt: tx.createdAt,
    };
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private async _mapTxWithDetails(tx: any) {
    const base = this._mapTx(tx);
    const [warehouse, item, batch] = await Promise.all([
      this.warehouseModel.findById(tx.warehouseId).lean().exec(),
      this.itemModel.findById(tx.itemId).lean().exec(),
      this.batchModel.findById(tx.batchId).lean().exec(),
    ]);
    return {
      ...base,
      warehouseName: warehouse?.name ?? null,
      itemCode: item?.itemCode ?? null,
      itemName: item?.itemName ?? null,
      batchNumber: batch?.batchNumber ?? null,
      expiryDate: base.expiryDate ?? batch?.expiryDate ?? null,
    };
  }
}
