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

/** PO-linked receipt line — itemId and unitId default from the PO line when omitted */
interface POLinkedGoodsReceiptLineInput {
  id?: string;
  purchaseOrderLineId: string;
  quantity: string;
  itemId?: string;
  unitId?: string;
  requiresInventoryHandoff?: boolean;
  destinationSite?: string | null;
  destinationStorageLocation?: string | null;
}

/** PO-unlinked receipt line — itemId and unitId must be provided explicitly */
interface POUnlinkedGoodsReceiptLineInput {
  id?: string;
  purchaseOrderLineId: null;
  quantity: string;
  itemId: string;
  unitId: string;
  requiresInventoryHandoff?: boolean;
  destinationSite?: string | null;
  destinationStorageLocation?: string | null;
}

type GoodsReceiptLineInput = POLinkedGoodsReceiptLineInput | POUnlinkedGoodsReceiptLineInput;

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]));

    // Fetch PO headers for receiving site fallback
    const poIds = [...new Set(orderLines.map((ol) => ol.purchaseOrderId))];
    const poHeaders =
      poIds.length > 0
        ? await db.selectFrom("PurchaseOrder").selectAll().where("id", "in", poIds).execute()
        : [];
    const poMap = new Map(poHeaders.map((po) => [po.id, po]));

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

    const insertValues: Insertable<"GoodsReceiptLine">[] = [];
    for (const line of lines) {
      const {
        id: _lineId,
        purchaseOrderLineId,
        quantity,
        itemId: lineItemId,
        unitId: lineUnitId,
        requiresInventoryHandoff,
        destinationSite,
        destinationStorageLocation,
        ...lineCustomFields
      } = line;

      if (new Decimal(quantity).lte(0)) {
        return err(new InvalidReceivedQuantityError(purchaseOrderLineId ?? "unknown"));
      }

      const orderLine =
        purchaseOrderLineId != null ? orderLineMap.get(purchaseOrderLineId) : undefined;

      if (purchaseOrderLineId != null) {
        if (!orderLine) {
          return err(new PurchaseOrderLineNotFoundError(purchaseOrderLineId));
        }

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

      const resolvedUnitId = lineUnitId ?? orderLine?.unitId;
      if (resolvedUnitId == null) {
        return err(new PurchaseOrderLineNotFoundError(purchaseOrderLineId ?? "unknown"));
      }

      insertValues.push({
        ...lineCustomFields,
        goodsReceiptId: id,
        purchaseOrderLineId: purchaseOrderLineId ?? null,
        itemId: lineItemId ?? orderLine?.itemId ?? null,
        quantity,
        unitId: resolvedUnitId,
        requiresInventoryHandoff:
          requiresInventoryHandoff ?? orderLine?.requiresPhysicalReceipt ?? true,
        destinationSite:
          destinationSite ??
          orderLine?.receivingSiteId ??
          (orderLine ? poMap.get(orderLine.purchaseOrderId)?.receivingSiteId : null) ??
          null,
        destinationStorageLocation: destinationStorageLocation ?? null,
        createdAt: new Date(),
        updatedAt: null,
      });
    }

    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(insertValues).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 });
}
