import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import { inventoryExecutionLifecycle } from "../db/inventoryExecution.lifecycle.generated";
import type { Transaction } from "../generated/kysely-tailordb";
import { consumeReceiptInventorySupplyPlansInternal } from "../internal/inventorySupplyPlan";
import {
  EmptyReceiptLinesError,
  InvalidStatusError,
  InvalidUnitCostError,
  ItemNotFoundError,
  LotNotFoundError,
  SerialNotFoundError,
  StorageLocationNotFoundError,
} from "../lib/errors.generated";
import { applyValuation } from "../lib/valuation";
import type { ItemManagementQueries } from "../module";

export interface PostGoodsReceiptInput {
  id: string;
}

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

  if (!inventoryExecution || inventoryExecution.executionType !== "GOODS_RECEIPT") {
    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 EmptyReceiptLinesError(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));
      }
    }

    if (line.serialNumberId) {
      const serial = await db
        .selectFrom("SerialNumber")
        .selectAll()
        .where("id", "=", line.serialNumberId)
        .executeTakeFirst();
      if (!serial) {
        return err(new SerialNotFoundError(line.serialNumberId));
      }
    }

    const unitCost = line.unitCost ?? "0";
    if (new Decimal(unitCost).lt(0)) {
      return err(new InvalidUnitCostError(unitCost));
    }

    const supplyPlanResult = await consumeReceiptInventorySupplyPlansInternal(db, {
      itemId: line.itemId,
      siteId: location.siteId,
      quantity: line.primaryQuantity,
      sourceDocumentType: inventoryExecution.sourceDocumentType,
      sourceDocumentId: inventoryExecution.sourceDocumentId,
      sourceLineId: line.sourceLineId,
      lotId: line.lotId ?? null,
      serialNumberId: line.serialNumberId ?? null,
    });
    if (!supplyPlanResult.ok) {
      return supplyPlanResult;
    }

    const ledgerEntry = await db
      .insertInto("InventoryLedger")
      .values({
        inventoryExecutionId: inventoryExecution.id,
        dcIndicator: "DR",
        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);

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

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

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

    if (line.lotId) {
      const lotStockLevel = await db
        .selectFrom("LotStockLevel")
        .selectAll()
        .where("lotId", "=", line.lotId)
        .where("storageLocationId", "=", line.storageLocationId)
        .forUpdate()
        .executeTakeFirst();

      if (lotStockLevel) {
        await db
          .updateTable("LotStockLevel")
          .set({ onHand: new Decimal(lotStockLevel.onHand).plus(line.primaryQuantity).toString() })
          .where("id", "=", lotStockLevel.id)
          .execute();
      } else {
        await db
          .insertInto("LotStockLevel")
          .values({
            lotId: line.lotId,
            storageLocationId: line.storageLocationId,
            onHand: line.primaryQuantity,
            blocked: "0",
          })
          .execute();
      }
    }
  }

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

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