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 {
  RotationTransaction,
  RotationTransactionDocument,
  RotationInLine,
  RotationOutLine,
} from "./schemas/rotation-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;
const MT_TOLERANCE = 0.001;

@Injectable()
export class RotationTransactionsService {
  constructor(
    @InjectModel(RotationTransaction.name)
    private readonly rotationModel: Model<RotationTransactionDocument>,
    @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: {
    warehouseId: string;
    vendorId: string;
    voucher: string;
    rotationIn: Array<{
      itemId: string;
      batchNumber: string;
      expiryDate?: string;
      productionDate?: string;
      quantityMt: number;
    }>;
    rotationOut: Array<{
      itemId: string;
      batchId: string;
      quantityMt: number;
    }>;
    submittedBy?: string;
  }) {
    if (!payload.warehouseId) throw new BadRequestException("warehouseId is required");
    if (!payload.vendorId) throw new BadRequestException("vendorId is required");
    if (!payload.voucher) throw new BadRequestException("voucher is required");
    if (!payload.rotationIn || payload.rotationIn.length === 0) {
      throw new BadRequestException("At least one Rotation In item is required");
    }
    if (!payload.rotationOut || payload.rotationOut.length === 0) {
      throw new BadRequestException("At least one Rotation Out item is required");
    }

    const vendor = await this.vendorModel.findById(payload.vendorId).lean().exec();
    if (!vendor) throw new BadRequestException("Vendor not found");

    payload.rotationIn.forEach((line, idx) => {
      if (!line.itemId) throw new BadRequestException(`Rotation In line ${idx + 1}: itemId is required`);
      if (!line.batchNumber) throw new BadRequestException(`Rotation In line ${idx + 1}: batch number is required`);
      if (!line.quantityMt || line.quantityMt <= 0) {
        throw new BadRequestException(`Rotation In line ${idx + 1}: quantityMt must be greater than 0`);
      }
    });

    const inTotal = payload.rotationIn.reduce((s, i) => s + (i.quantityMt || 0), 0);
    const outTotal = payload.rotationOut.reduce((s, o) => s + (o.quantityMt || 0), 0);
    if (Math.abs(outTotal - inTotal) > MT_TOLERANCE) {
      throw new BadRequestException(
        `Rotation In (${inTotal} MT) must equal total Rotation Out (${outTotal} MT).`,
      );
    }

    // Resolve/create each rotation-in batch (mirrors createStockIn's find-or-create)
    const rotationIn: RotationInLine[] = await Promise.all(
      payload.rotationIn.map(async (line) => {
        const batch = await this._findOrCreateBatch(
          payload.warehouseId,
          line.itemId,
          line.batchNumber,
          line.expiryDate ?? "",
          line.productionDate ?? null,
        );
        return {
          itemId: new Types.ObjectId(line.itemId),
          batchId: batch._id as Types.ObjectId,
          batchNumber: line.batchNumber,
          expiryDate: line.expiryDate ?? null,
          productionDate: line.productionDate ?? null,
          quantityMt: line.quantityMt,
        };
      }),
    );

    // Resolve each rotation-out line against its (existing) batch
    const rotationOut: RotationOutLine[] = await Promise.all(
      payload.rotationOut.map(async (o, idx) => {
        if (!o.itemId) throw new BadRequestException(`Rotation Out line ${idx + 1}: itemId is required`);
        if (!o.batchId) throw new BadRequestException(`Rotation Out line ${idx + 1}: batchId is required`);
        if (!o.quantityMt || o.quantityMt <= 0) {
          throw new BadRequestException(`Rotation Out line ${idx + 1}: quantityMt must be greater than 0`);
        }
        const batch = await this.batchModel.findById(o.batchId).lean().exec();
        if (!batch) throw new BadRequestException(`Rotation Out line ${idx + 1}: batch not found`);

        return {
          itemId: new Types.ObjectId(o.itemId),
          batchId: new Types.ObjectId(o.batchId),
          batchNumber: batch.batchNumber,
          quantityMt: o.quantityMt,
        };
      }),
    );

    const rotation = await this.rotationModel.create({
      warehouseId: new Types.ObjectId(payload.warehouseId),
      vendorId: new Types.ObjectId(payload.vendorId),
      vendor: vendor.name,
      voucher: payload.voucher,
      rotationIn,
      rotationOut,
      totalMt: inTotal,
      approvalStatus: "pending_approval",
      submittedBy: payload.submittedBy ?? null,
      createdAt: new Date().toISOString(),
    });

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

    return this._mapRotation(rotation.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.rotationModel.find(query).sort({ createdAt: -1 });
    if (params?.page && params?.limit) {
      queryBuilder = queryBuilder.skip((params.page - 1) * params.limit).limit(params.limit);
    }

    const rotations = await queryBuilder.lean().exec();
    return Promise.all(rotations.map((r) => this._mapRotationWithDetails(r)));
  }

  async findOne(id: string) {
    const rotation = await this.rotationModel.findById(id).lean().exec();
    if (!rotation) throw new BadRequestException("Rotation not found");
    return this._mapRotationWithDetails(rotation);
  }

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

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

    const inLines = this._getInLines(rotation.toObject());

    // Validate every Rotation Out leg has sufficient balance before mutating anything
    for (const line of rotation.rotationOut as RotationOutLine[]) {
      const balance = await this.balanceModel.findOne({ batchId: line.batchId }).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`,
        );
      }
    }

    // Apply every Rotation In line (+) — all land in the shared destination warehouse
    for (const line of inLines) {
      await this._applyBalanceDelta(
        rotation.warehouseId.toString(),
        line.itemId,
        line.batchId,
        line.quantityMt,
        "inbound",
      );
    }

    // Apply every Rotation Out leg (-) — resolve each leg's own warehouse from its
    // batch, since Rotation Out can draw from a different warehouse than
    // Rotation In's target warehouse.
    for (const line of rotation.rotationOut as RotationOutLine[]) {
      const batch = await this.batchModel.findById(line.batchId).lean().exec();
      const legWarehouseId = batch?.warehouseId?.toString() ?? rotation.warehouseId.toString();
      await this._applyBalanceDelta(
        legWarehouseId,
        line.itemId.toString(),
        line.batchId.toString(),
        line.quantityMt,
        "outbound",
      );
    }

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

    return this._mapRotation(rotation.toObject());
  }

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

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

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

    return this._mapRotation(rotation.toObject());
  }

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

  // Normalizes Rotation In lines whether they're stored as the current
  // `rotationIn[]` array or (for documents created before that field existed)
  // the legacy flat `inItemId`/`inBatchId`/... fields on the root document.
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private _getInLines(r: any): Array<{
    itemId: string;
    batchId: string;
    batchNumber: string;
    expiryDate: string | null;
    productionDate: string | null;
    quantityMt: number;
  }> {
    if (Array.isArray(r.rotationIn) && r.rotationIn.length > 0) {
      return r.rotationIn.map((line: RotationInLine) => ({
        itemId: line.itemId.toString(),
        batchId: line.batchId.toString(),
        batchNumber: line.batchNumber,
        expiryDate: line.expiryDate ?? null,
        productionDate: line.productionDate ?? null,
        quantityMt: line.quantityMt,
      }));
    }
    if (r.inItemId) {
      return [
        {
          itemId: r.inItemId.toString(),
          batchId: r.inBatchId.toString(),
          batchNumber: r.inBatchNumber,
          expiryDate: r.inExpiryDate ?? null,
          productionDate: null,
          quantityMt: r.inQuantityMt,
        },
      ];
    }
    return [];
  }

  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 _mapRotation(r: any) {
    const inLines = this._getInLines(r);
    return {
      id: (r._id as Types.ObjectId)?.toString() ?? r.id,
      warehouseId: r.warehouseId?.toString(),
      vendorId: r.vendorId?.toString() ?? null,
      vendor: r.vendor ?? null,
      voucher: r.voucher ?? null,
      rotationIn: inLines,
      rotationOut: (r.rotationOut ?? []).map((line: RotationOutLine) => ({
        itemId: line.itemId?.toString(),
        batchId: line.batchId?.toString(),
        batchNumber: line.batchNumber,
        quantityMt: line.quantityMt,
      })),
      totalMt: r.totalMt,
      approvalStatus: r.approvalStatus,
      approvedBy: r.approvedBy ?? null,
      approvedAt: r.approvedAt ?? null,
      rejectedBy: r.rejectedBy ?? null,
      rejectedAt: r.rejectedAt ?? null,
      rejectedReason: r.rejectedReason ?? null,
      submittedBy: r.submittedBy ?? null,
      createdAt: r.createdAt,
    };
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private async _mapRotationWithDetails(r: any) {
    const base = this._mapRotation(r);
    const warehouse = await this.warehouseModel.findById(r.warehouseId).lean().exec();

    const rotationInDetailed = await Promise.all(
      base.rotationIn.map(async (line: { itemId: string; batchId: string; batchNumber: string; expiryDate: string | null; productionDate: string | null; quantityMt: number }) => {
        const item = await this.itemModel.findById(line.itemId).lean().exec();
        return {
          itemId: line.itemId,
          itemCode: item?.itemCode ?? null,
          itemName: item?.itemName ?? item?.description ?? null,
          batchId: line.batchId,
          batchNumber: line.batchNumber,
          expiryDate: line.expiryDate,
          productionDate: line.productionDate,
          quantityMt: line.quantityMt,
        };
      }),
    );
    const rotationOutDetailed = await Promise.all(
      base.rotationOut.map(async (line: { itemId: string; batchId: string; batchNumber: string; quantityMt: number }) => {
        const item = await this.itemModel.findById(line.itemId).lean().exec();
        const batch = await this.batchModel.findById(line.batchId).lean().exec();
        const sourceWarehouse = batch?.warehouseId
          ? await this.warehouseModel.findById(batch.warehouseId).lean().exec()
          : null;
        return {
          itemId: line.itemId,
          itemCode: item?.itemCode ?? null,
          itemName: item?.itemName ?? item?.description ?? null,
          batchId: line.batchId,
          batchNumber: line.batchNumber,
          expiryDate: batch?.expiryDate ?? null,
          productionDate: batch?.productionDate ?? null,
          sourceWarehouseName: sourceWarehouse?.name ?? null,
          quantityMt: line.quantityMt,
        };
      }),
    );

    return {
      ...base,
      warehouseName: warehouse?.name ?? null,
      // Back-compat top-level fields for the first Rotation In line — the list
      // table shows one summary row per rotation and reads these.
      inItemCode: rotationInDetailed[0]?.itemCode ?? null,
      inItemName: rotationInDetailed[0]?.itemName ?? null,
      rotationIn: rotationInDetailed,
      rotationOut: rotationOutDetailed,
    };
  }
}
