import { Decimal } from "decimal.js";
import type { defineModule as defineBusinessPartnerModule } from "../../business-partner";
import type { Transaction } from "../generated/kysely-tailordb";
type BusinessPartnerModule = ReturnType<typeof defineBusinessPartnerModule>;
type BusinessPartnerQueries = BusinessPartnerModule["queries"];

import type { defineModule as defineItemManagementModule } from "../../item-management";
type ItemManagementModule = ReturnType<typeof defineItemManagementModule>;
type ItemManagementQueries = ItemManagementModule["queries"];

import type { defineModule as defineInventoryModule } from "../../inventory";
type InventoryModule = ReturnType<typeof defineInventoryModule>;
type InventoryCommands = InventoryModule["commands"];

import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import { goodsReceiptLifecycle } from "../db/goodsReceipt.lifecycle.generated";
import { deriveReceiptStatus, type LineProgress } from "../lib/domain";
import {
  GoodsReceiptNotDraftError,
  GoodsReceiptNotFoundError,
  InactiveExecutionReferenceError,
  StockMovementCreationFailedError,
  NoOpenReceiptQuantityError,
  OverReceiptToleranceExceededError,
  PurchaseOrderNotReceivableError,
} from "../lib/errors.generated";

export interface PostGoodsReceiptInput {
  id: string;
}

export async function run(
  db: Transaction,
  input: PostGoodsReceiptInput,
  ctx: CommandContext,
  businessPartnerQueries: Pick<BusinessPartnerQueries, "getPartner">,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
  inventoryCommands: Pick<InventoryCommands, "receiveStock"> &
    Partial<Pick<InventoryCommands, "receiveStockWithInspection">>,
) {
  const goodsReceipt = await db
    .selectFrom("GoodsReceipt")
    .selectAll()
    .where("id", "=", input.id)
    .forUpdate()
    .executeTakeFirst();
  if (!goodsReceipt) {
    return err(new GoodsReceiptNotFoundError(input.id));
  }
  const nextStatus = goodsReceiptLifecycle.tryTransition(goodsReceipt.status, "post");
  if (!nextStatus) {
    return err(new GoodsReceiptNotDraftError(input.id));
  }

  const { partner: supplier } = (
    await businessPartnerQueries.getPartner(db, { id: goodsReceipt.supplierId }, ctx)
  ).value;
  if (supplier?.status !== "ACTIVE") {
    return err(new InactiveExecutionReferenceError(goodsReceipt.supplierId));
  }

  const receiptLines = await db
    .selectFrom("GoodsReceiptLine")
    .selectAll()
    .where("goodsReceiptId", "=", goodsReceipt.id)
    .execute();

  // Separate receipt lines into PO-linked and PO-unlinked
  const poLinkedReceiptLines = receiptLines.filter(
    (rl): rl is typeof rl & { purchaseOrderLineId: string } => rl.purchaseOrderLineId != null,
  );
  const poLineIds = [...new Set(poLinkedReceiptLines.map((rl) => rl.purchaseOrderLineId))];

  // Fetch referenced PO lines (stable IDs — no lineage resolution needed)
  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]));

  // Collect unique PO IDs from PO lines
  const poIds = [...new Set(orderLines.map((ol) => ol.purchaseOrderId))];

  // Fetch all referenced POs
  const purchaseOrders =
    poIds.length > 0
      ? await db
          .selectFrom("PurchaseOrder")
          .selectAll()
          .where("id", "in", poIds)
          .forUpdate()
          .execute()
      : [];

  // Validate all referenced POs are receivable
  for (const po of purchaseOrders) {
    if (["CANCELLED", "CLOSED"].includes(po.orderStatus)) {
      return err(new PurchaseOrderNotReceivableError(po.id));
    }
  }

  // Compute already-received quantities from POSTED GoodsReceipts
  const postedGRIds = new Set(
    (await db.selectFrom("GoodsReceipt").selectAll().where("status", "=", "POSTED").execute()).map(
      (r) => r.id,
    ),
  );

  // Fetch receipt lines for the referenced PO lines
  const allReceiptLinesForPoLines =
    poLineIds.length > 0
      ? await db
          .selectFrom("GoodsReceiptLine")
          .selectAll()
          .where("purchaseOrderLineId", "in", poLineIds)
          .execute()
      : [];

  // Received quantity per PO line ID (direct, no lineage)
  const receivedByLineId = new Map<string, Decimal>();
  for (const grl of allReceiptLinesForPoLines) {
    if (grl.purchaseOrderLineId && postedGRIds.has(grl.goodsReceiptId)) {
      receivedByLineId.set(
        grl.purchaseOrderLineId,
        (receivedByLineId.get(grl.purchaseOrderLineId) ?? new Decimal(0)).plus(grl.quantity),
      );
    }
  }

  // Collect stock-item receipt lines that require inventory handoff to receiveStock
  const stockReceiptLines: {
    itemId: string;
    storageLocationId: string;
    quantity: string;
    unitCost: string;
  }[] = [];

  for (const receiptLine of poLinkedReceiptLines) {
    const poLineId = receiptLine.purchaseOrderLineId;
    const orderLine = orderLineMap.get(poLineId);
    if (!orderLine) {
      return err(new NoOpenReceiptQuantityError(poLineId));
    }
    // Stable ID — no SUPERSEDED check needed; deleted lines won't be in orderLineMap
    const alreadyReceived = receivedByLineId.get(poLineId) ?? new Decimal(0);
    const openQuantity = new Decimal(orderLine.quantity).minus(alreadyReceived);
    if (openQuantity.lte(0)) {
      return err(new NoOpenReceiptQuantityError(poLineId));
    }
    if (new Decimal(receiptLine.quantity).gt(openQuantity)) {
      return err(new OverReceiptToleranceExceededError(poLineId));
    }

    if (receiptLine.requiresInventoryHandoff) {
      if (!receiptLine.destinationStorageLocation || orderLine.unitPrice == null) {
        return err(new StockMovementCreationFailedError(input.id));
      }
      stockReceiptLines.push({
        itemId: orderLine.itemId,
        storageLocationId: receiptLine.destinationStorageLocation,
        quantity: receiptLine.quantity,
        unitCost: orderLine.unitPrice,
      });
    }
  }

  // Execute stock receipt for each stock-item receipt line. When the caller wired
  // up `receiveStockWithInspection`, route through it (inspection-hold receipt,
  // 3 ledger entries, stock lands blocked). Otherwise route through `receiveStock`
  // (standard receipt, 1 ledger entry, stock becomes immediately available).
  const receiveCmd = inventoryCommands.receiveStockWithInspection ?? inventoryCommands.receiveStock;
  for (const line of stockReceiptLines) {
    const receiveResult = await receiveCmd(
      db,
      {
        itemId: line.itemId,
        storageLocationId: line.storageLocationId,
        quantity: line.quantity,
        unitCost: line.unitCost,
        sourceId: input.id,
      },
      ctx,
    );
    if (!receiveResult.ok) {
      return err(new StockMovementCreationFailedError(input.id));
    }
  }

  // Post the GR
  const posted = await db
    .updateTable("GoodsReceipt")
    .set({
      status: nextStatus,
      updatedAt: new Date(),
    })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  // Derive PO receiptStatus (no lineage — direct per-line aggregation)
  postedGRIds.add(goodsReceipt.id);

  for (const poId of poIds) {
    const poLines = await db
      .selectFrom("PurchaseOrderLine")
      .selectAll()
      .where("purchaseOrderId", "=", poId)
      .execute();

    const poLineIdsForRollup = poLines.map((ol) => ol.id);
    const receiptLinesForPo =
      poLineIdsForRollup.length > 0
        ? await db
            .selectFrom("GoodsReceiptLine")
            .selectAll()
            .where("purchaseOrderLineId", "in", poLineIdsForRollup)
            .execute()
        : [];

    const receivedForRollup = new Map<string, Decimal>();
    for (const grl of receiptLinesForPo) {
      if (grl.purchaseOrderLineId && postedGRIds.has(grl.goodsReceiptId)) {
        receivedForRollup.set(
          grl.purchaseOrderLineId,
          (receivedForRollup.get(grl.purchaseOrderLineId) ?? new Decimal(0)).plus(grl.quantity),
        );
      }
    }

    const lineProgress: LineProgress[] = poLines.map((ol) => ({
      quantity: ol.quantity,
      receivedQuantity: (receivedForRollup.get(ol.id) ?? new Decimal(0)).toString(),
    }));

    await db
      .updateTable("PurchaseOrder")
      .set({
        receiptStatus: deriveReceiptStatus(lineProgress),
        updatedAt: new Date(),
      })
      .where("id", "=", poId)
      .returningAll()
      .executeTakeFirstOrThrow();
  }

  return ok({ goodsReceipt: posted });
}
