import { Decimal } from "decimal.js";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  GoodsReceiptNotDraftError,
  GoodsReceiptNotFoundError,
  InvalidReceivedQuantityError,
  NoOpenReceiptQuantityError,
  PurchaseOrderLineNotFoundError,
} from "../lib/errors.generated";
import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";

interface GoodsReceiptLineInput {
  id?: string;
  purchaseOrderLineId?: string | null;
  quantity: string;
  itemId?: string;
  unitId?: string | null;
  requiresInventoryHandoff?: boolean;
  destinationSite?: string | null;
  destinationStorageLocation?: string | null;
}

export type UpdateGoodsReceiptInput = {
  id: string;
  receiptDate?: Date;
  lines?: GoodsReceiptLineInput[];
};
export async function run<CF extends Record<string, unknown>, LCF extends Record<string, unknown>>(
  db: Transaction,
  input: Omit<UpdateGoodsReceiptInput, "lines"> &
    Partial<CF> & { lines?: (GoodsReceiptLineInput & LCF)[] },
  ctx: CommandContext,
) {
  void ctx;
  const { id, receiptDate, lines, ...customFields } = input;
  const goodsReceipt = await db
    .selectFrom("GoodsReceipt")
    .selectAll()
    .where("id", "=", id)
    .forUpdate()
    .executeTakeFirst();
  if (!goodsReceipt) {
    return err(new GoodsReceiptNotFoundError(id));
  }
  if (goodsReceipt.status !== "DRAFT") {
    return err(new GoodsReceiptNotDraftError(id));
  }

  if (lines !== undefined) {
    // Separate PO-linked and PO-unlinked lines
    const poLinkedLines = lines.filter((l) => l.purchaseOrderLineId != null);
    const poLineIds = [...new Set(poLinkedLines.map((l) => l.purchaseOrderLineId!))];

    // Fetch all referenced PO lines in one query
    const orderLines =
      poLineIds.length > 0
        ? await db
            .selectFrom("PurchaseOrderLine")
            .selectAll()
            .where("id", "in", poLineIds)
            .execute()
        : [];

    const orderLineMap = new Map(orderLines.map((ol) => [ol.id, ol]));

    // Compute already-received quantities from POSTED GoodsReceipts (bulk)
    const alreadyReceivedByPoLineId = new Map<string, Decimal>();
    if (poLineIds.length > 0) {
      const existingGRLines = await db
        .selectFrom("GoodsReceiptLine")
        .selectAll()
        .where("purchaseOrderLineId", "in", poLineIds)
        .execute();

      const postedGRIds = new Set(
        (
          await db.selectFrom("GoodsReceipt").selectAll().where("status", "=", "POSTED").execute()
        ).map((r) => r.id),
      );

      for (const grl of existingGRLines) {
        if (grl.purchaseOrderLineId && postedGRIds.has(grl.goodsReceiptId)) {
          alreadyReceivedByPoLineId.set(
            grl.purchaseOrderLineId,
            (alreadyReceivedByPoLineId.get(grl.purchaseOrderLineId) ?? new Decimal(0)).plus(
              grl.quantity,
            ),
          );
        }
      }
    }

    for (const line of lines) {
      if (new Decimal(line.quantity).lte(0)) {
        return err(new InvalidReceivedQuantityError(line.purchaseOrderLineId ?? "unknown"));
      }
      if (line.purchaseOrderLineId == null) continue;

      const orderLine = orderLineMap.get(line.purchaseOrderLineId);
      if (!orderLine) {
        return err(new PurchaseOrderLineNotFoundError(line.purchaseOrderLineId));
      }

      // Check open quantity
      const openQuantity = new Decimal(orderLine.quantity).minus(
        alreadyReceivedByPoLineId.get(line.purchaseOrderLineId) ?? new Decimal(0),
      );
      if (openQuantity.lte(0)) {
        return err(new NoOpenReceiptQuantityError(line.purchaseOrderLineId));
      }
    }

    const updated = await db
      .updateTable("GoodsReceipt")
      .set({
        ...(customFields as Record<string, unknown>),
        receiptDate: receiptDate ?? goodsReceipt.receiptDate,
        updatedAt: new Date(),
      })
      .where("id", "=", id)
      .returningAll()
      .executeTakeFirstOrThrow();

    await db.deleteFrom("GoodsReceiptLine").where("goodsReceiptId", "=", id).execute();
    await db
      .insertInto("GoodsReceiptLine")
      .values(
        lines.map((line) => {
          const {
            id: _lineId,
            purchaseOrderLineId,
            quantity,
            itemId: lineItemId,
            unitId: lineUnitId,
            requiresInventoryHandoff,
            destinationSite,
            destinationStorageLocation,
            ...lineCustomFields
          } = line;
          const orderLine =
            purchaseOrderLineId != null ? orderLineMap.get(purchaseOrderLineId) : undefined;
          return {
            ...(lineCustomFields as Record<string, unknown>),
            goodsReceiptId: id,
            purchaseOrderLineId: purchaseOrderLineId ?? null,
            itemId: lineItemId ?? orderLine?.itemId ?? null,
            quantity,
            unitId: lineUnitId ?? orderLine?.unitId ?? null,
            requiresInventoryHandoff:
              requiresInventoryHandoff ?? orderLine?.requiresPhysicalReceipt ?? true,
            destinationSite: destinationSite ?? orderLine?.receivingSiteId ?? null,
            destinationStorageLocation: destinationStorageLocation ?? null,
            createdAt: new Date(),
            updatedAt: null,
          };
        }),
      )
      .execute();

    return ok({ goodsReceipt: updated });
  }

  const updated = await db
    .updateTable("GoodsReceipt")
    .set({
      ...(customFields as Record<string, unknown>),
      receiptDate: receiptDate ?? goodsReceipt.receiptDate,
      updatedAt: new Date(),
    })
    .where("id", "=", id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ goodsReceipt: updated });
}
