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 {
  TransferTransaction,
  TransferTransactionDocument,
  TransferLine,
} from "./schemas/transfer-transaction.schema";
import { Vendor, VendorDocument } from "./schemas/vendor.schema";
import { Warehouse, WarehouseDocument } from "./schemas/warehouse.schema";
import { NotificationsService } from "./notifications.service";

const GLOBAL_MT_THRESHOLD = 26000;

@Injectable()
export class TransferTransactionsService {
  constructor(
    @InjectModel(TransferTransaction.name)
    private readonly transferModel: Model<TransferTransactionDocument>,
    @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(Vendor.name)
    private readonly vendorModel: Model<VendorDocument>,
    private readonly notificationsService: NotificationsService,
  ) {}

  // ── Create ────────────────────────────────────────────────────────────────

  async create(payload: {
    destinationWarehouseId: string;
    vendorId?: string;
    lines: Array<{ itemId: string; sourceBatchId: string; quantityMt: number; destinationBatchNumber?: string }>;
    submittedBy?: string;
  }) {
    if (!payload.destinationWarehouseId) throw new BadRequestException("destinationWarehouseId is required");
    if (!payload.lines || payload.lines.length === 0) {
      throw new BadRequestException("At least one line is required");
    }

    let vendor: { name: string } | null = null;
    if (payload.vendorId) {
      vendor = await this.vendorModel.findById(payload.vendorId).lean().exec();
      if (!vendor) throw new BadRequestException("Vendor not found");
    }

    const lines: TransferLine[] = await Promise.all(
      payload.lines.map(async (line, idx) => {
        if (!line.itemId) throw new BadRequestException(`Line ${idx + 1}: itemId is required`);
        if (!line.sourceBatchId) throw new BadRequestException(`Line ${idx + 1}: sourceBatchId is required`);
        if (!line.quantityMt || line.quantityMt <= 0) {
          throw new BadRequestException(`Line ${idx + 1}: quantityMt must be greater than 0`);
        }
        const batch = await this.batchModel.findById(line.sourceBatchId).lean().exec();
        if (!batch) throw new BadRequestException(`Line ${idx + 1}: source batch not found`);
        if (batch.warehouseId.toString() === payload.destinationWarehouseId) {
          throw new BadRequestException(
            `Line ${idx + 1}: source and destination warehouse are the same — nothing to transfer`,
          );
        }

        return {
          itemId: new Types.ObjectId(line.itemId),
          sourceBatchId: new Types.ObjectId(line.sourceBatchId),
          sourceWarehouseId: batch.warehouseId,
          batchNumber: batch.batchNumber,
          destinationBatchNumber: line.destinationBatchNumber?.trim() || batch.batchNumber,
          expiryDate: batch.expiryDate ?? null,
          productionDate: batch.productionDate ?? null,
          quantityMt: line.quantityMt,
        };
      }),
    );

    const totalMt = lines.reduce((s, l) => s + l.quantityMt, 0);

    const transfer = await this.transferModel.create({
      destinationWarehouseId: new Types.ObjectId(payload.destinationWarehouseId),
      vendorId: payload.vendorId ? new Types.ObjectId(payload.vendorId) : null,
      vendor: vendor?.name ?? null,
      lines,
      totalMt,
      approvalStatus: "pending_approval",
      submittedBy: payload.submittedBy ?? null,
      createdAt: new Date().toISOString(),
    });

    await this.notificationsService.notifyManagers(
      `New Transfer of ${totalMt} MT is pending your approval.`,
      (transfer._id as Types.ObjectId).toString(),
    );

    return this._mapTransfer(transfer.toObject());
  }

  // ── Read ──────────────────────────────────────────────────────────────────

  async findAll(params?: { status?: string; page?: number; limit?: number }) {
    const query: Record<string, unknown> = {};
    if (params?.status) query.approvalStatus = params.status;

    let queryBuilder = this.transferModel.find(query).sort({ createdAt: -1 });
    if (params?.page && params?.limit) {
      queryBuilder = queryBuilder.skip((params.page - 1) * params.limit).limit(params.limit);
    }

    const transfers = await queryBuilder.lean().exec();
    return Promise.all(transfers.map((t) => this._mapTransferWithDetails(t)));
  }

  async findOne(id: string) {
    const transfer = await this.transferModel.findById(id).lean().exec();
    if (!transfer) throw new BadRequestException("Transfer not found");
    return this._mapTransferWithDetails(transfer);
  }

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

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

    const lines = transfer.lines as TransferLine[];

    // Validate every line has sufficient balance at its source before mutating anything
    for (const line of lines) {
      const balance = await this.balanceModel.findOne({ batchId: line.sourceBatchId }).exec();
      if (!balance || balance.currentQuantityMt < line.quantityMt) {
        throw new BadRequestException(
          `Insufficient inventory for batch ${line.batchNumber}. Available: ${balance?.currentQuantityMt ?? 0} MT, Requested: ${line.quantityMt} MT`,
        );
      }
    }

    for (const line of lines) {
      // Move out of the source batch
      await this._applyBalanceDelta(
        line.sourceWarehouseId.toString(),
        line.itemId.toString(),
        line.sourceBatchId.toString(),
        line.quantityMt,
        "outbound",
      );

      // Find-or-create the destination batch — same batch number as the
      // source unless this line was explicitly relabeled on creation.
      const destBatch = await this._findOrCreateBatch(
        transfer.destinationWarehouseId.toString(),
        line.itemId.toString(),
        line.destinationBatchNumber || line.batchNumber,
        line.expiryDate ?? "",
        line.productionDate ?? null,
      );
      await this._applyBalanceDelta(
        transfer.destinationWarehouseId.toString(),
        line.itemId.toString(),
        (destBatch._id as Types.ObjectId).toString(),
        line.quantityMt,
        "inbound",
      );
    }

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

    return this._mapTransfer(transfer.toObject());
  }

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

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

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

    return this._mapTransfer(transfer.toObject());
  }

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

  private async _findOrCreateBatch(
    warehouseId: string,
    itemId: string,
    batchNumber: string,
    expiryDate: string,
    productionDate: string | null = null,
  ) {
    const existing = await this.batchModel
      .findOne({
        warehouseId: new Types.ObjectId(warehouseId),
        itemId: new Types.ObjectId(itemId),
        batchNumber,
      })
      .exec();
    if (existing) {
      if (expiryDate) {
        existing.expiryDate = expiryDate;
        await existing.save();
      }
      return existing;
    }
    return this.batchModel.create({
      warehouseId: new Types.ObjectId(warehouseId),
      itemId: new Types.ObjectId(itemId),
      batchNumber,
      expiryDate: expiryDate || "2099-12-31",
      productionDate,
    });
  }

  private async _applyBalanceDelta(
    warehouseId: string,
    itemId: string,
    batchId: string,
    quantityMt: number,
    type: "inbound" | "outbound",
  ) {
    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(),
      });
    }

    const delta = type === "inbound" ? quantityMt : -quantityMt;
    balance.currentQuantityMt = Math.max(0, balance.currentQuantityMt + delta);
    balance.lastUpdated = new Date().toISOString();
    await balance.save();

    if (type === "outbound" && balance.currentQuantityMt === 0) {
      const batch = await this.batchModel.findById(batchId).exec();
      if (batch) {
        batch.stockedOut = true;
        await batch.save();
      }
    }
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private _mapTransfer(t: any) {
    return {
      id: (t._id as Types.ObjectId)?.toString() ?? t.id,
      destinationWarehouseId: t.destinationWarehouseId?.toString(),
      vendorId: t.vendorId?.toString() ?? null,
      vendor: t.vendor ?? null,
      lines: (t.lines ?? []).map((line: TransferLine) => ({
        itemId: line.itemId?.toString(),
        sourceBatchId: line.sourceBatchId?.toString(),
        sourceWarehouseId: line.sourceWarehouseId?.toString(),
        batchNumber: line.batchNumber,
        destinationBatchNumber: line.destinationBatchNumber ?? line.batchNumber,
        expiryDate: line.expiryDate ?? null,
        productionDate: line.productionDate ?? null,
        quantityMt: line.quantityMt,
      })),
      totalMt: t.totalMt,
      approvalStatus: t.approvalStatus,
      approvedBy: t.approvedBy ?? null,
      approvedAt: t.approvedAt ?? null,
      rejectedBy: t.rejectedBy ?? null,
      rejectedAt: t.rejectedAt ?? null,
      rejectedReason: t.rejectedReason ?? null,
      submittedBy: t.submittedBy ?? null,
      createdAt: t.createdAt,
    };
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private async _mapTransferWithDetails(t: any) {
    const base = this._mapTransfer(t);
    const destinationWarehouse = await this.warehouseModel.findById(t.destinationWarehouseId).lean().exec();

    const linesDetailed = await Promise.all(
      base.lines.map(async (line: { itemId: string; sourceBatchId: string; sourceWarehouseId: string; batchNumber: string; destinationBatchNumber: string; expiryDate: string | null; productionDate: string | null; quantityMt: number }) => {
        const [item, sourceWarehouse] = await Promise.all([
          this.itemModel.findById(line.itemId).lean().exec(),
          this.warehouseModel.findById(line.sourceWarehouseId).lean().exec(),
        ]);
        return {
          itemId: line.itemId,
          itemCode: item?.itemCode ?? null,
          itemName: item?.itemName ?? item?.description ?? null,
          sourceBatchId: line.sourceBatchId,
          sourceWarehouseId: line.sourceWarehouseId,
          sourceWarehouseName: sourceWarehouse?.name ?? null,
          batchNumber: line.batchNumber,
          destinationBatchNumber: line.destinationBatchNumber,
          expiryDate: line.expiryDate,
          productionDate: line.productionDate,
          quantityMt: line.quantityMt,
        };
      }),
    );

    return {
      ...base,
      destinationWarehouseName: destinationWarehouse?.name ?? null,
      itemSummary: linesDetailed[0]?.itemName ?? linesDetailed[0]?.itemCode ?? null,
      lines: linesDetailed,
    };
  }
}
