import { Decimal } from "decimal.js";
import type { Transaction } from "../generated/kysely-tailordb";
import type { defineModule as defineBusinessPartnerModule } from "../../business-partner";
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 {
  GoodsReceiptNotDraftError,
  GoodsReceiptNotFoundError,
  InactiveExecutionReferenceError,
  NoOpenReceiptQuantityError,
  OverReceiptToleranceExceededError,
  PurchaseOrderNotReceivableError,
  StockMovementCreationFailedError,
  SupersededLineReferencedError,
} from "../lib/errors.generated";
import { deriveReceiptStatus, type LineProgress } from "../lib/domain";
import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";

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, "createStockMovement">,
) {
  const goodsReceipt = await db
    .selectFrom("GoodsReceipt")
    .selectAll()
    .where("id", "=", input.id)
    .forUpdate()
    .executeTakeFirst();
  if (!goodsReceipt) {
    return err(new GoodsReceiptNotFoundError(input.id));
  }
  if (goodsReceipt.status !== "DRAFT") {
    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.purchaseOrderLineId !== null);
  const poLineIds = [...new Set(poLinkedReceiptLines.map((rl) => rl.purchaseOrderLineId!))];

  // Fetch all referenced PO lines
  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 (current DRAFT GR excluded)
  const postedGRIds = new Set(
    (await db.selectFrom("GoodsReceipt").selectAll().where("status", "=", "POSTED").execute()).map(
      (r) => r.id,
    ),
  );

  // Fetch ALL PO lines for lineage-aware computation (ACTIVE + SUPERSEDED)
  const allPoLinesForLineage =
    poIds.length > 0
      ? await db
          .selectFrom("PurchaseOrderLine")
          .selectAll()
          .where("purchaseOrderId", "in", poIds)
          .execute()
      : [];
  const allPoLineIdsForLineage = allPoLinesForLineage.map((l) => l.id);

  const allReceiptLinesForPoLines =
    allPoLineIdsForLineage.length > 0
      ? await db
          .selectFrom("GoodsReceiptLine")
          .selectAll()
          .where("purchaseOrderLineId", "in", allPoLineIdsForLineage)
          .execute()
      : [];

  // Raw received quantity per individual line ID
  const rawReceivedByLineId = new Map<string, Decimal>();
  for (const grl of allReceiptLinesForPoLines) {
    if (grl.purchaseOrderLineId && postedGRIds.has(grl.goodsReceiptId)) {
      rawReceivedByLineId.set(
        grl.purchaseOrderLineId,
        (rawReceivedByLineId.get(grl.purchaseOrderLineId) ?? new Decimal(0)).plus(grl.quantity),
      );
    }
  }

  // Helper: get lineage IDs for a given line
  function getLineageIds(line: (typeof allPoLinesForLineage)[number]) {
    const rootId = line.rootLineId ?? line.id;
    return allPoLinesForLineage
      .filter((l) => l.id === rootId || l.rootLineId === rootId)
      .map((l) => l.id);
  }

  // Helper: compute lineage-aware received quantity
  function getLineageReceived(line: (typeof allPoLinesForLineage)[number]) {
    const lineageIds = getLineageIds(line);
    return lineageIds.reduce(
      (sum, id) => sum.plus(rawReceivedByLineId.get(id) ?? 0),
      new Decimal(0),
    );
  }

  const stockMoveLines: {
    itemId: string;
    quantity: number;
    destinationLocationId: string | null;
  }[] = [];

  for (const receiptLine of poLinkedReceiptLines) {
    const poLineId = receiptLine.purchaseOrderLineId!;
    const orderLine = orderLineMap.get(poLineId);
    if (!orderLine) {
      return err(new NoOpenReceiptQuantityError(poLineId));
    }
    if ((orderLine.lineStatus ?? "ACTIVE") === "SUPERSEDED") {
      return err(new SupersededLineReferencedError(poLineId));
    }
    // Use lineage-aware received quantity for over-receipt validation
    const lineageLineData = allPoLinesForLineage.find((l) => l.id === poLineId);
    const alreadyReceived = lineageLineData
      ? getLineageReceived(lineageLineData)
      : (rawReceivedByLineId.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) {
      stockMoveLines.push({
        itemId: orderLine.itemId,
        quantity: Number(receiptLine.quantity),
        destinationLocationId: receiptLine.destinationStorageLocation ?? null,
      });
    }
  }

  // Create a DRAFT stock movement in inventory for lines that require it
  if (stockMoveLines.length > 0) {
    const stockMoveResult = await inventoryCommands.createStockMovement(
      db,
      {
        movementType: "GOODS_RECEIPT",
        lines: stockMoveLines,
      },
      ctx,
    );
    if (!stockMoveResult.ok) {
      return err(new StockMovementCreationFailedError(input.id));
    }
  }

  // Post the GR — after this, receipt lines count toward received totals
  const posted = await db
    .updateTable("GoodsReceipt")
    .set({
      status: "POSTED",
      updatedAt: new Date(),
    })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  // Derive PO receiptStatus from source of truth (GoodsReceiptLine + POSTED GRs)
  postedGRIds.add(goodsReceipt.id);

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

    const activePoLines = allPoLines.filter((l) => (l.lineStatus ?? "ACTIVE") === "ACTIVE");
    const allPoLineIdsForRollup = allPoLines.map((ol) => ol.id);
    const receiptLinesForPo =
      allPoLineIdsForRollup.length > 0
        ? await db
            .selectFrom("GoodsReceiptLine")
            .selectAll()
            .where("purchaseOrderLineId", "in", allPoLineIdsForRollup)
            .execute()
        : [];

    // Raw received per individual line ID
    const rawReceivedForRollup = new Map<string, Decimal>();
    for (const grl of receiptLinesForPo) {
      if (grl.purchaseOrderLineId && postedGRIds.has(grl.goodsReceiptId)) {
        rawReceivedForRollup.set(
          grl.purchaseOrderLineId,
          (rawReceivedForRollup.get(grl.purchaseOrderLineId) ?? new Decimal(0)).plus(grl.quantity),
        );
      }
    }

    // Lineage-aware: only ACTIVE lines as targets, sum received across lineage
    const lineProgress: LineProgress[] = activePoLines.map((ol) => {
      const rootId = ol.rootLineId ?? ol.id;
      const lineageIds = allPoLines
        .filter((l) => l.id === rootId || l.rootLineId === rootId)
        .map((l) => l.id);
      const totalReceived = lineageIds.reduce(
        (sum, id) => sum.plus(rawReceivedForRollup.get(id) ?? 0),
        new Decimal(0),
      );
      return {
        quantity: ol.quantity,
        receivedQuantity: totalReceived.toString(),
      };
    });

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

  return ok({ goodsReceipt: posted });
}
