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 ReceiveTransferOrderInput {
  id: string;
  effectiveDate: Date;
  lines: {
    transferOrderLineId: string;
    transitStorageLocationId: string;
    destinationStorageLocationId: string;
    quantity: string;
  }[];
}

export async function run(db: Transaction, input: ReceiveTransferOrderInput, _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: ReceiveTransferOrderInput["lines"][number];
    quantity: Decimal;
    line: Selectable<"TransferOrderLine">;
    transitLocation: Selectable<"StorageLocation">;
    destinationLocation: Selectable<"StorageLocation">;
    transitStockLevel: Selectable<"StockLevel">;
    transitStockQuantity: Decimal;
    supplyPlanConsumptions: {
      supplyPlan: Selectable<"InventorySupplyPlan">;
      receivedQuantity: Decimal;
    }[];
  }[] = [];
  const requestedReceiveByLineId = new Map<string, Decimal>();
  const requestedReceiveByLineTransitLocationId = new Map<string, Decimal>();
  const requestedTransitByStockLevelId = new Map<string, Decimal>();
  const remainingSupplyPlanQuantityById = 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 inTransitQuantity = new Decimal(line.shippedQuantity).minus(line.receivedQuantity);
    const requestedReceiveQuantity = (requestedReceiveByLineId.get(line.id) ?? new Decimal(0)).plus(
      quantity,
    );
    if (requestedReceiveQuantity.gt(inTransitQuantity)) {
      return err(new InvalidQuantityError(lineInput.quantity));
    }
    requestedReceiveByLineId.set(line.id, requestedReceiveQuantity);

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

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

    const transitStockLevel = await db
      .selectFrom("StockLevel")
      .selectAll()
      .where("itemId", "=", line.itemId)
      .where("storageLocationId", "=", transitLocation.id)
      .where("stockType", "=", "IN_TRANSIT")
      .forUpdate()
      .executeTakeFirst();
    const transitStockQuantity = new Decimal(transitStockLevel?.quantity ?? 0);
    if (!transitStockLevel) {
      return err(new InsufficientStockError(`${line.itemId}:${transitLocation.id}`));
    }
    const requestedTransitQuantity = (
      requestedTransitByStockLevelId.get(transitStockLevel.id) ?? new Decimal(0)
    ).plus(quantity);
    if (transitStockQuantity.lt(requestedTransitQuantity)) {
      return err(new InsufficientStockError(`${line.itemId}:${transitLocation.id}`));
    }
    requestedTransitByStockLevelId.set(transitStockLevel.id, requestedTransitQuantity);

    const transitExecutionLines = await db
      .selectFrom("InventoryExecutionLine")
      .innerJoin(
        "InventoryExecution",
        "InventoryExecution.id",
        "InventoryExecutionLine.inventoryExecutionId",
      )
      .select(["InventoryExecutionLine.direction", "InventoryExecutionLine.primaryQuantity"])
      .where("InventoryExecution.sourceDocumentType", "=", "TRANSFER_ORDER")
      .where("InventoryExecution.sourceDocumentId", "=", transferOrder.id)
      .where("InventoryExecution.status", "=", "POSTED")
      .where("InventoryExecutionLine.sourceLineId", "=", line.id)
      .where("InventoryExecutionLine.itemId", "=", line.itemId)
      .where("InventoryExecutionLine.storageLocationId", "=", transitLocation.id)
      .where("InventoryExecutionLine.stockType", "=", "IN_TRANSIT")
      .execute();
    const lineTransitQuantity = transitExecutionLines.reduce((total, executionLine) => {
      const executionQuantity = new Decimal(executionLine.primaryQuantity);
      return executionLine.direction === "IN"
        ? total.plus(executionQuantity)
        : total.minus(executionQuantity);
    }, new Decimal(0));
    const lineTransitKey = `${line.id}:${transitLocation.id}`;
    const requestedReceiveLineTransitQuantity = (
      requestedReceiveByLineTransitLocationId.get(lineTransitKey) ?? new Decimal(0)
    ).plus(quantity);
    if (lineTransitQuantity.lt(requestedReceiveLineTransitQuantity)) {
      return err(new InsufficientStockError(`${line.itemId}:${transitLocation.id}`));
    }
    requestedReceiveByLineTransitLocationId.set(
      lineTransitKey,
      requestedReceiveLineTransitQuantity,
    );

    const matchingSupplyPlans = await db
      .selectFrom("InventorySupplyPlan")
      .selectAll()
      .where("itemId", "=", line.itemId)
      .where("siteId", "=", destinationLocation.siteId)
      .where("sourceType", "=", "TRANSFER_ORDER")
      .where("sourceId", "=", transferOrder.id)
      .where("sourceLineId", "=", line.id)
      .where("status", "=", "OPEN")
      .orderBy("expectedDate", "asc")
      .orderBy("id", "asc")
      .forUpdate()
      .execute();
    const applicableSupplyPlans = matchingSupplyPlans.filter(
      (supplyPlan) =>
        (supplyPlan.lotId === null || supplyPlan.lotId === (line.lotId ?? null)) &&
        (supplyPlan.serialNumberId === null ||
          supplyPlan.serialNumberId === (line.serialNumberId ?? null)),
    );
    const supplyPlanConsumptions: {
      supplyPlan: Selectable<"InventorySupplyPlan">;
      receivedQuantity: Decimal;
    }[] = [];
    let remainingSupplyPlanConsumption = quantity;
    for (const supplyPlan of applicableSupplyPlans) {
      if (remainingSupplyPlanConsumption.lte(0)) break;

      const openQuantity =
        remainingSupplyPlanQuantityById.get(supplyPlan.id) ??
        Decimal.max(new Decimal(supplyPlan.expectedQuantity).minus(supplyPlan.receivedQuantity), 0);
      const receivedQuantity = Decimal.min(openQuantity, remainingSupplyPlanConsumption);
      if (receivedQuantity.lte(0)) continue;

      supplyPlanConsumptions.push({ supplyPlan, receivedQuantity });
      remainingSupplyPlanQuantityById.set(supplyPlan.id, openQuantity.minus(receivedQuantity));
      remainingSupplyPlanConsumption = remainingSupplyPlanConsumption.minus(receivedQuantity);
    }

    validatedLines.push({
      input: lineInput,
      quantity,
      line,
      transitLocation,
      destinationLocation,
      transitStockLevel,
      transitStockQuantity,
      supplyPlanConsumptions,
    });
  }

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

  const inventoryExecutionLineValues = [];
  const ledgerEntryValues = [];
  const transferOrderLines = [];
  const transitQuantityByStockLevelId = new Map<string, Decimal>();
  const receivedQuantityByLineId = new Map<string, Decimal>();
  const receivedQuantityBySupplyPlanId = new Map<
    string,
    { supplyPlan: Selectable<"InventorySupplyPlan">; receivedQuantity: Decimal }
  >();

  for (const {
    input: lineInput,
    quantity,
    line,
    transitLocation,
    destinationLocation,
    transitStockLevel,
    transitStockQuantity,
    supplyPlanConsumptions,
  } 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: transitLocation.id,
        stockType: "IN_TRANSIT" 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: destinationLocation.id,
        stockType: "AVAILABLE" 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: transitLocation.id,
        stockType: "IN_TRANSIT" 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: destinationLocation.id,
        stockType: "AVAILABLE" as const,
        quantity: lineInput.quantity,
        executedAt: now,
        lotId: line.lotId ?? null,
        serialNumberId: line.serialNumberId ?? null,
      },
    );

    for (const { supplyPlan, receivedQuantity } of supplyPlanConsumptions) {
      const current = receivedQuantityBySupplyPlanId.get(supplyPlan.id);
      receivedQuantityBySupplyPlanId.set(supplyPlan.id, {
        supplyPlan,
        receivedQuantity: (current?.receivedQuantity ?? new Decimal(0)).plus(receivedQuantity),
      });
    }

    const transitQuantity = (
      transitQuantityByStockLevelId.get(transitStockLevel.id) ?? transitStockQuantity
    ).minus(quantity);
    transitQuantityByStockLevelId.set(transitStockLevel.id, transitQuantity);
    await db
      .updateTable("StockLevel")
      .set({ quantity: transitQuantity.toString() })
      .where("id", "=", transitStockLevel.id)
      .execute();

    const destinationStockLevel = await db
      .selectFrom("StockLevel")
      .selectAll()
      .where("itemId", "=", line.itemId)
      .where("storageLocationId", "=", destinationLocation.id)
      .where("stockType", "=", "AVAILABLE")
      .forUpdate()
      .executeTakeFirst();

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

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

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

  if (receivedQuantityBySupplyPlanId.size > 0) {
    for (const { supplyPlan, receivedQuantity } of receivedQuantityBySupplyPlanId.values()) {
      await db
        .updateTable("InventorySupplyPlan")
        .set({
          receivedQuantity: new Decimal(supplyPlan.receivedQuantity)
            .plus(receivedQuantity)
            .toString(),
          status: "OPEN",
        })
        .where("id", "=", supplyPlan.id)
        .execute();
    }
  }

  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,
  });
}
