import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import { inventoryExecutionLifecycle } from "../db/inventoryExecution.lifecycle.generated";
import { serialNumberLifecycle } from "../db/serialNumber.lifecycle.generated";
import type { Transaction } from "../generated/kysely-tailordb";
import { consumeGoodsIssueReservationsInternal } from "../internal/stockReservation";
import {
  EmptyIssueLinesError,
  InsufficientLotStockError,
  InvalidStatusError,
  ItemNotFoundError,
  LotNotFoundError,
  SerialNotAvailableError,
  SerialNotFoundError,
  StorageLocationNotFoundError,
} from "../lib/errors.generated";
import { applyValuation } from "../lib/valuation";
import type { ItemManagementQueries } from "../module";

export interface PostGoodsIssueInput {
  id: string;
}

export async function run(
  db: Transaction,
  input: PostGoodsIssueInput,
  ctx: CommandContext,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
) {
  const inventoryExecution = await db
    .selectFrom("InventoryExecution")
    .selectAll()
    .where("id", "=", input.id)
    .forUpdate()
    .executeTakeFirst();

  if (!inventoryExecution || inventoryExecution.executionType !== "GOODS_ISSUE") {
    return err(new InvalidStatusError(input.id));
  }
  const nextStatus = inventoryExecutionLifecycle.tryTransition(inventoryExecution.status, "post");
  if (!nextStatus) {
    return err(new InvalidStatusError(input.id));
  }

  const executionLines = await db
    .selectFrom("InventoryExecutionLine")
    .selectAll()
    .where("inventoryExecutionId", "=", inventoryExecution.id)
    .execute();
  if (executionLines.length === 0) {
    return err(new EmptyIssueLinesError(input.id));
  }

  const allLedgerEntries = [];
  const now = new Date();

  for (const line of executionLines) {
    const { item } = (await itemManagementQueries.getItem(db, { id: line.itemId }, ctx)).value;
    if (!item) {
      return err(new ItemNotFoundError(line.itemId));
    }

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

    if (line.lotId) {
      const lot = await db
        .selectFrom("Lot")
        .selectAll()
        .where("id", "=", line.lotId)
        .executeTakeFirst();
      if (!lot) {
        return err(new LotNotFoundError(line.lotId));
      }
    }

    const serial = line.serialNumberId
      ? await db
          .selectFrom("SerialNumber")
          .selectAll()
          .where("id", "=", line.serialNumberId)
          .executeTakeFirst()
      : undefined;
    if (line.serialNumberId) {
      if (!serial) {
        return err(new SerialNotFoundError(line.serialNumberId));
      }
      const nextSerialStatus = serialNumberLifecycle.tryTransition(serial.status, "postGoodsIssue");
      if (!nextSerialStatus) {
        return err(new SerialNotAvailableError(line.serialNumberId));
      }
    }

    const issueReservationResult = await consumeGoodsIssueReservationsInternal(db, {
      itemId: line.itemId,
      siteId: location.siteId,
      storageLocationId: line.storageLocationId,
      stockType: line.stockType,
      quantity: line.primaryQuantity,
      sourceDocumentType: inventoryExecution.sourceDocumentType,
      sourceDocumentId: inventoryExecution.sourceDocumentId,
      sourceLineId: line.sourceLineId,
    });
    if (!issueReservationResult.ok) {
      return issueReservationResult;
    }
    const { stockLevel, locationStockQuantity } = issueReservationResult.value;

    const lotStockLevel = line.lotId
      ? await db
          .selectFrom("LotStockLevel")
          .selectAll()
          .where("lotId", "=", line.lotId)
          .where("storageLocationId", "=", line.storageLocationId)
          .forUpdate()
          .executeTakeFirst()
      : undefined;
    const lotOnHand = lotStockLevel ? new Decimal(lotStockLevel.onHand) : new Decimal(0);
    if (line.lotId && (!lotStockLevel || lotOnHand.lt(line.primaryQuantity))) {
      return err(new InsufficientLotStockError(`${line.lotId}:${line.storageLocationId}`));
    }

    const ledgerEntry = await db
      .insertInto("InventoryLedger")
      .values({
        inventoryExecutionId: inventoryExecution.id,
        dcIndicator: "CR",
        itemId: line.itemId,
        storageLocationId: line.storageLocationId,
        stockType: line.stockType,
        quantity: line.primaryQuantity,
        executedAt: now,
        lotId: line.lotId ?? null,
        serialNumberId: line.serialNumberId ?? null,
      })
      .returningAll()
      .executeTakeFirstOrThrow();
    allLedgerEntries.push(ledgerEntry);

    await db
      .updateTable("StockLevel")
      .set({ quantity: locationStockQuantity.minus(line.primaryQuantity).toString() })
      .where("id", "=", stockLevel.id)
      .execute();

    await applyValuation(db, {
      itemId: line.itemId,
      inventoryLedgerId: ledgerEntry.id,
      dcIndicator: "CR",
      quantity: line.primaryQuantity,
    });

    if (lotStockLevel) {
      await db
        .updateTable("LotStockLevel")
        .set({ onHand: lotOnHand.minus(line.primaryQuantity).toString() })
        .where("id", "=", lotStockLevel.id)
        .execute();
    }

    if (serial) {
      await db
        .updateTable("SerialNumber")
        .set({ status: serialNumberLifecycle.transitions.postGoodsIssue.to })
        .where("id", "=", serial.id)
        .execute();
    }
  }

  const postedInventoryExecution = await db
    .updateTable("InventoryExecution")
    .set({ status: nextStatus, postedAt: now })
    .where("id", "=", inventoryExecution.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({
    inventoryExecution: postedInventoryExecution,
    inventoryLedgerEntries: allLedgerEntries,
  });
}
