import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import type { Selectable, Transaction } from "../generated/kysely-tailordb";
import {
  EmptyPurchaseOrderLinesError,
  ItemNotActiveError,
  ItemNotFoundError,
  InvalidQuantityError,
  InvalidUnitPriceError,
  PartnerNotSupplierError,
  ReceivingSiteNotFoundError,
  ReceivingSiteRequiredError,
  SupplierNotActiveError,
  SupplierNotFoundError,
} from "../lib/errors.generated";
import type { BusinessPartnerQueries, ItemManagementQueries, OrganizationQueries } from "../module";

export interface PurchaseOrderLineInput {
  itemId: string;
  quantity: string;
  unitPrice: string;
  unitId: string;
  receivingSiteId?: string | null;
  requiresPhysicalReceipt?: boolean;
}

export interface PurchaseOrderHeaderInput {
  companyId: string;
  supplierId: string;
  orderDate: Date;
  receivingSiteId?: string | null;
  externalSupplierOrderReference?: string;
}

export interface CreatePurchaseOrderInput {
  header: PurchaseOrderHeaderInput;
  lines: PurchaseOrderLineInput[];
}

// The order's domain-owned state that invariants are checked over, minus DB-assigned fields.
type PurchaseOrderHeaderState = Omit<Selectable<"PurchaseOrder">, "id" | "createdAt" | "updatedAt">;

type PurchaseOrderLineState = Omit<
  Selectable<"PurchaseOrderLine">,
  "id" | "createdAt" | "updatedAt" | "purchaseOrderId"
>;

function checkPurchaseOrderInvariants(
  id: string,
  header: PurchaseOrderHeaderState,
  lines: PurchaseOrderLineState[],
) {
  if (lines.length === 0) {
    return new EmptyPurchaseOrderLinesError(id);
  }
  for (const line of lines) {
    if (new Decimal(line.quantity).lte(0)) {
      return new InvalidQuantityError(line.itemId);
    }
    if (new Decimal(line.unitPrice).lt(0)) {
      return new InvalidUnitPriceError(line.itemId);
    }
    if (line.requiresPhysicalReceipt && !line.receivingSiteId && !header.receivingSiteId) {
      return new ReceivingSiteRequiredError(line.itemId);
    }
  }
  return null;
}

export async function run<CF extends Record<string, unknown>, LCF extends Record<string, unknown>>(
  db: Transaction,
  input: {
    header: PurchaseOrderHeaderInput & CF;
    lines: (PurchaseOrderLineInput & LCF)[];
  },
  ctx: CommandContext,
  businessPartnerQueries: Pick<BusinessPartnerQueries, "getPartner" | "getPartnerRole">,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
  organizationQueries: Pick<OrganizationQueries, "getSite">,
) {
  const { header, lines } = input;

  // ===== 1. Validate the referenced master data =====
  // Supplier — must exist, be ACTIVE, and hold the SUPPLIER role.
  const { partner: supplier } = (
    await businessPartnerQueries.getPartner(db, { id: header.supplierId }, ctx)
  ).value;
  if (!supplier) {
    return err(new SupplierNotFoundError(header.supplierId));
  }
  if (supplier.status !== "ACTIVE") {
    return err(new SupplierNotActiveError(header.supplierId));
  }
  const { partnerRole } = (
    await businessPartnerQueries.getPartnerRole(
      db,
      { partnerId: header.supplierId, role: "SUPPLIER" },
      ctx,
    )
  ).value;
  if (!partnerRole) {
    return err(new PartnerNotSupplierError(header.supplierId));
  }

  // Items — must exist and be ACTIVE; snapshot its name and SKU.
  const itemSnapshotByItemId = new Map<string, { name: string; sku: string }>();
  for (const line of lines) {
    if (itemSnapshotByItemId.has(line.itemId)) {
      continue;
    }
    const { item } = (await itemManagementQueries.getItem(db, { id: line.itemId }, ctx)).value;
    if (!item) {
      return err(new ItemNotFoundError(line.itemId));
    }
    if (item.status !== "ACTIVE") {
      return err(new ItemNotActiveError(line.itemId));
    }
    itemSnapshotByItemId.set(line.itemId, { name: item.name, sku: item.sku });
  }
  // Every item validated above is in the map, so this lookup never misses.
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  const itemSnapshotFor = (itemId: string) => itemSnapshotByItemId.get(itemId)!;

  // Receiving sites — every referenced site must exist.
  const siteIdsToValidate = new Set<string>();
  if (header.receivingSiteId != null) {
    siteIdsToValidate.add(header.receivingSiteId);
  }
  for (const line of lines) {
    if (line.receivingSiteId != null) {
      siteIdsToValidate.add(line.receivingSiteId);
    }
  }
  for (const siteId of siteIdsToValidate) {
    const { site } = (await organizationQueries.getSite(db, { id: siteId }, ctx)).value;
    if (!site) {
      return err(new ReceivingSiteNotFoundError(siteId));
    }
  }

  // ===== 2. Reconstitute the new order and check its invariants =====
  const mergedLines: PurchaseOrderLineState[] = lines.map((line) => {
    const itemSnapshot = itemSnapshotFor(line.itemId);
    return {
      itemId: line.itemId,
      itemSnapshotName: itemSnapshot.name,
      itemSnapshotSku: itemSnapshot.sku,
      quantity: line.quantity,
      unitPrice: line.unitPrice,
      unitId: line.unitId,
      receivingSiteId: line.receivingSiteId ?? null,
      requiresPhysicalReceipt: line.requiresPhysicalReceipt ?? true,
      billedQuantity: null,
    };
  });

  const mergedHeader: PurchaseOrderHeaderState = {
    companyId: header.companyId,
    supplierId: header.supplierId,
    receivingSiteId: header.receivingSiteId ?? null,
    orderStatus: "DRAFT",
    receiptStatus: null,
    billingStatus: null,
    orderDate: header.orderDate,
    externalSupplierOrderReference: header.externalSupplierOrderReference ?? null,
    supplierSnapshotName: supplier.name,
    rejectionReason: null,
    closeReason: null,
  };

  const violation = checkPurchaseOrderInvariants(header.companyId, mergedHeader, mergedLines);
  if (violation) {
    return err(violation);
  }

  // ===== 3. Persist the order =====
  // Header — input fields first (incl. custom fields) so command-set columns always win.
  const purchaseOrder = await db
    .insertInto("PurchaseOrder")
    .values({ ...(header as Record<string, unknown>), ...mergedHeader })
    .returningAll()
    .executeTakeFirstOrThrow();

  // Lines — input fields first (incl. custom fields) so command-set columns always win.
  await db
    .insertInto("PurchaseOrderLine")
    .values(
      mergedLines.map((mergedLine, index) => ({
        ...(lines[index] as Record<string, unknown>),
        ...mergedLine,
        purchaseOrderId: purchaseOrder.id,
      })),
    )
    .execute();

  return ok({ purchaseOrder });
}
