import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import { purchaseRequisitionLifecycle } from "../db/purchaseRequisition.lifecycle.generated";
import type { Transaction } from "../generated/kysely-tailordb";
import {
  PartnerNotSupplierError,
  PurchaseRequisitionAlreadyConvertedError,
  PurchaseRequisitionNotApprovedError,
  PurchaseRequisitionNotFoundError,
  ReceivingSiteNotFoundError,
  SupplierNotActiveError,
  SupplierNotFoundError,
  SupplierRequiredError,
} from "../lib/errors.generated";
import type { BusinessPartnerQueries, ItemManagementQueries, OrganizationQueries } from "../module";

export interface ConvertPurchaseRequisitionToPurchaseOrderInput {
  id: string;
  supplierId?: string;
  receivingSiteId?: string | null;
}

export async function run(
  db: Transaction,
  input: ConvertPurchaseRequisitionToPurchaseOrderInput,
  ctx: CommandContext,
  businessPartnerQueries: Pick<BusinessPartnerQueries, "getPartner" | "getPartnerRole">,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
  organizationQueries: Pick<OrganizationQueries, "getSite">,
) {
  // 1. Read requisition with forUpdate lock
  const requisition = await db
    .selectFrom("PurchaseRequisition")
    .selectAll()
    .where("id", "=", input.id)
    .forUpdate()
    .executeTakeFirst();
  if (!requisition) {
    return err(new PurchaseRequisitionNotFoundError(input.id));
  }

  // 2. Validate status
  if (requisition.status === "CONVERTED") {
    return err(new PurchaseRequisitionAlreadyConvertedError(input.id));
  }
  const nextStatus = purchaseRequisitionLifecycle.tryTransition(requisition.status, "convert");
  if (!nextStatus) {
    return err(new PurchaseRequisitionNotApprovedError(input.id));
  }

  // 3. Read requisition lines
  const requisitionLines = await db
    .selectFrom("PurchaseRequisitionLine")
    .selectAll()
    .where("purchaseRequisitionId", "=", input.id)
    .execute();

  // 4. Resolve supplier: use input.supplierId, or fallback to first line's suggestedSupplierId
  const supplierId =
    input.supplierId ??
    (requisitionLines.length > 0 ? requisitionLines[0].suggestedSupplierId : null);
  if (!supplierId) {
    return err(new SupplierRequiredError(input.id));
  }

  // 5. Validate supplier (exists, active, SUPPLIER role)
  const { partner: supplier } = (
    await businessPartnerQueries.getPartner(db, { id: supplierId }, ctx)
  ).value;
  if (!supplier) {
    return err(new SupplierNotFoundError(supplierId));
  }
  if (supplier.status !== "ACTIVE") {
    return err(new SupplierNotActiveError(supplierId));
  }
  const { partnerRole } = (
    await businessPartnerQueries.getPartnerRole(
      db,
      { partnerId: supplierId, role: "SUPPLIER" },
      ctx,
    )
  ).value;
  if (!partnerRole) {
    return err(new PartnerNotSupplierError(supplierId));
  }

  const companyId = requisition.companyId;

  // 7. Validate receiving site
  if (input.receivingSiteId) {
    const { site } = (await organizationQueries.getSite(db, { id: input.receivingSiteId }, ctx))
      .value;
    if (!site) {
      return err(new ReceivingSiteNotFoundError(input.receivingSiteId));
    }
  }

  // 8. Resolve price rules for each line
  const priceRulesByItemId = new Map<string, { unitPrice: string }>();
  for (const line of requisitionLines) {
    const priceRule = await db
      .selectFrom("PurchasePriceRule")
      .selectAll()
      .where("itemId", "=", line.itemId)
      .where("status", "=", "ACTIVE")
      .executeTakeFirst();
    if (priceRule) {
      priceRulesByItemId.set(line.itemId, {
        unitPrice: priceRule.unitPrice,
      });
    }
  }

  // 8. Create PurchaseOrder (DRAFT status)
  const purchaseOrder = await db
    .insertInto("PurchaseOrder")
    .values({
      companyId,
      supplierId,
      receivingSiteId: input.receivingSiteId ?? null,
      orderStatus: "DRAFT",
      receiptStatus: null,
      billingStatus: null,
      orderDate: new Date(),
      externalSupplierOrderReference: null,
      supplierSnapshotName: supplier.name,
      rejectionReason: null,
      closeReason: null,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  // 9. For each requisition line, create PurchaseOrderLine + conversion record
  for (const line of requisitionLines) {
    const priceRule = priceRulesByItemId.get(line.itemId);
    const unitPrice = priceRule ? priceRule.unitPrice : line.estimatedUnitPrice;

    const { item } = (await itemManagementQueries.getItem(db, { id: line.itemId }, ctx)).value;
    const itemSnapshotName = item?.name ?? null;
    const itemSnapshotSku = item?.sku ?? null;

    const poLine = await db
      .insertInto("PurchaseOrderLine")
      .values({
        purchaseOrderId: purchaseOrder.id,
        itemId: line.itemId,
        itemSnapshotName,
        itemSnapshotSku,

        quantity: line.quantity,
        unitPrice,
        unitId: line.unitId,
        receivingSiteId: null,
        requiresPhysicalReceipt: true,
      })
      .returningAll()
      .executeTakeFirstOrThrow();

    // Record the line-level allocation mapping
    await db
      .insertInto("PurchaseRequisitionLineAllocation")
      .values({
        purchaseRequisitionId: input.id,
        purchaseRequisitionLineId: line.id,
        purchaseOrderId: purchaseOrder.id,
        purchaseOrderLineId: poLine.id,
        allocatedQuantity: line.quantity,
      })
      .execute();
  }

  // 10. Set requisition status to CONVERTED
  const converted = await db
    .updateTable("PurchaseRequisition")
    .set({ status: nextStatus })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ purchaseRequisition: converted, purchaseOrder });
}
