import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import type { Selectable, Transaction } from "../generated/kysely-tailordb";
import {
  EmptyTransferLinesError,
  InsufficientStockError,
  InvalidQuantityError,
  InvalidStateTransitionError,
  InvalidTransferLocationsError,
  StorageLocationNotFoundError,
  TransferOrderLineNotFoundError,
  TransferOrderNotFoundError,
} from "../lib/errors.generated";

export interface ShipTransferOrderInput {
  id: string;
  effectiveDate: Date;
  lines: {
    transferOrderLineId: string;
    sourceStorageLocationId: string;
    transitStorageLocationId: string;
    quantity: string;
  }[];
}

export async function run(db: Transaction, input: ShipTransferOrderInput, _ctx: CommandContext) {
  const transferOrder = await db
    .selectFrom("TransferOrder")
    .selectAll()
    .where("id", "=", input.id)
    .forUpdate()
    .executeTakeFirst();
  if (!transferOrder) {
    return err(new TransferOrderNotFoundError(input.id));
  }
  if (transferOrder.status !== "OPEN") {
    return err(new InvalidStateTransitionError(input.id));
  }
  if (input.lines.length === 0) {
    return err(new EmptyTransferLinesError(input.id));
  }

  const validatedLines: {
    input: ShipTransferOrderInput["lines"][number];
    quantity: Decimal;
    line: Selectable<"TransferOrderLine">;
    sourceLocation: Selectable<"StorageLocation">;
    transitLocation: Selectable<"StorageLocation">;
    sourceStockLevel: Selectable<"StockLevel">;
    sourceStockQuantity: Decimal;
    reservationConsumptions: {
      reservation: Selectable<"StockReservation">;
      consumedQuantity: Decimal;
    }[];
  }[] = [];
  const requestedShipByLineId = new Map<string, Decimal>();
  const requestedSourceStockByStockLevelId = new Map<string, Decimal>();
  const requestedSiteAvailableByItemSite = new Map<string, Decimal>();
  const remainingReservationQuantityById = new Map<string, Decimal>();

  for (const lineInput of input.lines) {
    const quantity = new Decimal(lineInput.quantity);
    if (quantity.lte(0)) {
      return err(new InvalidQuantityError(lineInput.quantity));
    }

    const line = await db
      .selectFrom("TransferOrderLine")
      .selectAll()
      .where("id", "=", lineInput.transferOrderLineId)
      .where("transferOrderId", "=", transferOrder.id)
      .forUpdate()
      .executeTakeFirst();
    if (!line) {
      return err(new TransferOrderLineNotFoundError(lineInput.transferOrderLineId));
    }

    const remainingToShip = new Decimal(line.orderedQuantity).minus(line.shippedQuantity);
    const requestedShipQuantity = (requestedShipByLineId.get(line.id) ?? new Decimal(0)).plus(
      quantity,
    );
    if (requestedShipQuantity.gt(remainingToShip)) {
      return err(new InvalidQuantityError(lineInput.quantity));
    }
    requestedShipByLineId.set(line.id, requestedShipQuantity);

    const sourceLocation = await db
      .selectFrom("StorageLocation")
      .selectAll()
      .where("id", "=", lineInput.sourceStorageLocationId)
      .executeTakeFirst();
    if (!sourceLocation) {
      return err(new StorageLocationNotFoundError(lineInput.sourceStorageLocationId));
    }
    if (sourceLocation.siteId !== transferOrder.sourceSiteId) {
      return err(new InvalidTransferLocationsError(lineInput.sourceStorageLocationId));
    }

    const transitLocation = await db
      .selectFrom("StorageLocation")
      .selectAll()
      .where("id", "=", lineInput.transitStorageLocationId)
      .executeTakeFirst();
    if (!transitLocation) {
      return err(new StorageLocationNotFoundError(lineInput.transitStorageLocationId));
    }

    const sourceStockLevel = await db
      .selectFrom("StockLevel")
      .selectAll()
      .where("itemId", "=", line.itemId)
      .where("storageLocationId", "=", sourceLocation.id)
      .where("stockType", "=", "AVAILABLE")
      .forUpdate()
      .executeTakeFirst();
    if (!sourceStockLevel) {
      return err(new InsufficientStockError(`${line.itemId}:${sourceLocation.id}`));
    }

    const sourceStockQuantity = new Decimal(sourceStockLevel.quantity);
    const requestedSourceStockQuantity = (
      requestedSourceStockByStockLevelId.get(sourceStockLevel.id) ?? new Decimal(0)
    ).plus(quantity);
    if (sourceStockQuantity.lt(requestedSourceStockQuantity)) {
      return err(new InsufficientStockError(`${line.itemId}:${sourceLocation.id}`));
    }
    requestedSourceStockByStockLevelId.set(sourceStockLevel.id, requestedSourceStockQuantity);

    const locationIds = await db
      .selectFrom("StorageLocation")
      .select("id")
      .where("siteId", "=", sourceLocation.siteId)
      .where("status", "=", "ACTIVE")
      .execute();
    const sourceSiteStockLevels =
      locationIds.length > 0
        ? await db
            .selectFrom("StockLevel")
            .selectAll()
            .where("itemId", "=", line.itemId)
            .where(
              "storageLocationId",
              "in",
              locationIds.map((location) => location.id),
            )
            .where("stockType", "=", "AVAILABLE")
            .orderBy("storageLocationId", "asc")
            .forUpdate()
            .execute()
        : [];
    const openReservations = await db
      .selectFrom("StockReservation")
      .selectAll()
      .where("itemId", "=", line.itemId)
      .where("siteId", "=", sourceLocation.siteId)
      .where("status", "=", "OPEN")
      .orderBy("requiredDate", "asc")
      .forUpdate()
      .execute();
    const matchingReservations = openReservations.filter(
      (reservation) =>
        reservation.sourceType === "TRANSFER_ORDER" &&
        reservation.sourceId === transferOrder.id &&
        reservation.sourceLineId === line.id &&
        (reservation.storageLocationId === null ||
          reservation.storageLocationId === sourceLocation.id),
    );
    const matchingReservationIds = new Set(
      matchingReservations.map((reservation) => reservation.id),
    );
    const reservedByOtherDemand = openReservations.reduce(
      (total, reservation) =>
        matchingReservationIds.has(reservation.id)
          ? total
          : total.plus(
              Decimal.max(
                new Decimal(reservation.reservedQuantity).minus(reservation.consumedQuantity),
                0,
              ),
            ),
      new Decimal(0),
    );
    const sitePhysicalAvailable = sourceSiteStockLevels.reduce(
      (total, stockLevel) => total.plus(stockLevel.quantity),
      new Decimal(0),
    );
    const siteAvailableKey = `${line.itemId}:${sourceLocation.siteId}`;
    const requestedSiteAvailableQuantity = (
      requestedSiteAvailableByItemSite.get(siteAvailableKey) ?? new Decimal(0)
    ).plus(quantity);
    if (sitePhysicalAvailable.minus(reservedByOtherDemand).lt(requestedSiteAvailableQuantity)) {
      return err(new InsufficientStockError(`${line.itemId}:${sourceLocation.siteId}`));
    }
    requestedSiteAvailableByItemSite.set(siteAvailableKey, requestedSiteAvailableQuantity);

    const reservationConsumptions: {
      reservation: Selectable<"StockReservation">;
      consumedQuantity: Decimal;
    }[] = [];
    let remainingReservationConsumption = quantity;
    for (const reservation of matchingReservations) {
      if (remainingReservationConsumption.lte(0)) break;

      const openQuantity =
        remainingReservationQuantityById.get(reservation.id) ??
        Decimal.max(
          new Decimal(reservation.reservedQuantity).minus(reservation.consumedQuantity),
          0,
        );
      const consumedQuantity = Decimal.min(openQuantity, remainingReservationConsumption);
      if (consumedQuantity.lte(0)) continue;

      reservationConsumptions.push({ reservation, consumedQuantity });
      remainingReservationQuantityById.set(reservation.id, openQuantity.minus(consumedQuantity));
      remainingReservationConsumption = remainingReservationConsumption.minus(consumedQuantity);
    }

    validatedLines.push({
      input: lineInput,
      quantity,
      line,
      sourceLocation,
      transitLocation,
      sourceStockLevel,
      sourceStockQuantity,
      reservationConsumptions,
    });
  }

  const now = new Date();
  const inventoryExecution = await db
    .insertInto("InventoryExecution")
    .values({
      executionType: "GOODS_ISSUE",
      status: "POSTED",
      effectiveDate: input.effectiveDate,
      postedAt: now,
      sourceDocumentType: "TRANSFER_ORDER",
      sourceDocumentId: transferOrder.id,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  const inventoryExecutionLineValues = [];
  const ledgerEntryValues = [];
  const transferOrderLines = [];
  const sourceQuantityByStockLevelId = new Map<string, Decimal>();
  const shippedQuantityByLineId = new Map<string, Decimal>();

  for (const {
    input: lineInput,
    quantity,
    line,
    sourceLocation,
    transitLocation,
    sourceStockLevel,
    sourceStockQuantity,
    reservationConsumptions,
  } of validatedLines) {
    inventoryExecutionLineValues.push(
      {
        inventoryExecutionId: inventoryExecution.id,
        direction: "OUT" as const,
        itemId: line.itemId,
        quantity: lineInput.quantity,
        unitId: line.unitId,
        primaryQuantity: lineInput.quantity,
        primaryUnitId: line.unitId,
        unitConversionRate: "1",
        unitCost: null,
        storageLocationId: sourceLocation.id,
        stockType: "AVAILABLE" as const,
        sourceLineId: line.id,
        lotId: line.lotId ?? null,
        serialNumberId: line.serialNumberId ?? null,
      },
      {
        inventoryExecutionId: inventoryExecution.id,
        direction: "IN" as const,
        itemId: line.itemId,
        quantity: lineInput.quantity,
        unitId: line.unitId,
        primaryQuantity: lineInput.quantity,
        primaryUnitId: line.unitId,
        unitConversionRate: "1",
        unitCost: null,
        storageLocationId: transitLocation.id,
        stockType: "IN_TRANSIT" as const,
        sourceLineId: line.id,
        lotId: line.lotId ?? null,
        serialNumberId: line.serialNumberId ?? null,
      },
    );
    ledgerEntryValues.push(
      {
        inventoryExecutionId: inventoryExecution.id,
        dcIndicator: "CR" as const,
        itemId: line.itemId,
        storageLocationId: sourceLocation.id,
        stockType: "AVAILABLE" as const,
        quantity: lineInput.quantity,
        executedAt: now,
        lotId: line.lotId ?? null,
        serialNumberId: line.serialNumberId ?? null,
      },
      {
        inventoryExecutionId: inventoryExecution.id,
        dcIndicator: "DR" as const,
        itemId: line.itemId,
        storageLocationId: transitLocation.id,
        stockType: "IN_TRANSIT" as const,
        quantity: lineInput.quantity,
        executedAt: now,
        lotId: line.lotId ?? null,
        serialNumberId: line.serialNumberId ?? null,
      },
    );

    for (const { reservation, consumedQuantity } of reservationConsumptions) {
      await db
        .updateTable("StockReservation")
        .set({
          consumedQuantity: new Decimal(reservation.consumedQuantity)
            .plus(consumedQuantity)
            .toString(),
          status: "OPEN",
        })
        .where("id", "=", reservation.id)
        .execute();
    }

    const sourceQuantity = (
      sourceQuantityByStockLevelId.get(sourceStockLevel.id) ?? sourceStockQuantity
    ).minus(quantity);
    sourceQuantityByStockLevelId.set(sourceStockLevel.id, sourceQuantity);
    await db
      .updateTable("StockLevel")
      .set({ quantity: sourceQuantity.toString() })
      .where("id", "=", sourceStockLevel.id)
      .execute();

    const transitStockLevel = await db
      .selectFrom("StockLevel")
      .selectAll()
      .where("itemId", "=", line.itemId)
      .where("storageLocationId", "=", transitLocation.id)
      .where("stockType", "=", "IN_TRANSIT")
      .forUpdate()
      .executeTakeFirst();

    if (transitStockLevel) {
      await db
        .updateTable("StockLevel")
        .set({ quantity: new Decimal(transitStockLevel.quantity).plus(quantity).toString() })
        .where("id", "=", transitStockLevel.id)
        .execute();
    } else {
      await db
        .insertInto("StockLevel")
        .values({
          itemId: line.itemId,
          storageLocationId: transitLocation.id,
          stockType: "IN_TRANSIT",
          quantity: lineInput.quantity,
        })
        .execute();
    }

    const shippedQuantity = (
      shippedQuantityByLineId.get(line.id) ?? new Decimal(line.shippedQuantity)
    ).plus(quantity);
    shippedQuantityByLineId.set(line.id, shippedQuantity);

    const transferOrderLine = await db
      .updateTable("TransferOrderLine")
      .set({ shippedQuantity: shippedQuantity.toString() })
      .where("id", "=", line.id)
      .returningAll()
      .executeTakeFirstOrThrow();
    transferOrderLines.push(transferOrderLine);
  }

  const inventoryExecutionLines = await db
    .insertInto("InventoryExecutionLine")
    .values(inventoryExecutionLineValues)
    .returningAll()
    .execute();
  const ledgerEntries = await db
    .insertInto("InventoryLedger")
    .values(ledgerEntryValues)
    .returningAll()
    .execute();

  return ok({
    inventoryExecution,
    inventoryExecutionLines,
    inventoryLedgerEntries: ledgerEntries,
    transferOrderLines,
  });
}
