import { Decimal } from "decimal.js";
import type { Selectable, Transaction } from "../generated/kysely-tailordb";
import type { defineModule as defineBusinessPartnerModule } from "../../business-partner";
type BusinessPartnerModule = ReturnType<typeof defineBusinessPartnerModule>;
type BusinessPartnerQueries = BusinessPartnerModule["queries"];
import {
  DuplicateSupplierInvoiceNumberError,
  EmptyBillLinesError,
  InvalidBilledQuantityError,
  PaymentTermSnapshotMissingError,
  PurchaseOrderCancelledError,
  PurchaseOrderLineNotFoundError,
  SupplierNotFoundError,
  SupplierTaxIdentificationMissingError,
} from "../lib/errors.generated";
import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";

export interface PurchaseBillLineInput {
  purchaseOrderLineId?: string | null;
  quantity: string;
  billedUnitPrice?: string;
  taxCode?: string;
  taxableAmount?: string;
  taxAmount?: string;
}

export interface CreatePurchaseBillInput {
  companyId: string;
  supplierId: string;
  invoiceNumber: string;
  invoiceDate: Date;
  lines: PurchaseBillLineInput[];
  supplierTaxIdentification?: string;
}

export async function run<CF extends Record<string, unknown>, LCF extends Record<string, unknown>>(
  db: Transaction,
  input: Omit<CreatePurchaseBillInput, "lines"> & CF & { lines: (PurchaseBillLineInput & LCF)[] },
  ctx: CommandContext,
  businessPartnerQueries: Pick<BusinessPartnerQueries, "listPartnerIdentificationsByPartner">,
) {
  const {
    companyId,
    supplierId,
    invoiceNumber,
    invoiceDate,
    lines,
    supplierTaxIdentification: inputTaxId,
    ...customFields
  } = input;
  const supplier = await db
    .selectFrom("BusinessPartner")
    .selectAll()
    .where("id", "=", supplierId)
    .executeTakeFirst();
  if (!supplier) {
    return err(new SupplierNotFoundError(supplierId));
  }
  if (
    await db
      .selectFrom("PurchaseBill")
      .selectAll()
      .where("companyId", "=", companyId)
      .where("supplierId", "=", supplierId)
      .where("invoiceNumber", "=", invoiceNumber)
      .executeTakeFirst()
  ) {
    return err(new DuplicateSupplierInvoiceNumberError(invoiceNumber));
  }
  if (lines.length === 0) {
    return err(new EmptyBillLinesError(invoiceNumber));
  }

  const normalizedLines: {
    purchaseOrderLineId: string | null;
    quantity: string;
    billedUnitPrice: string | null;
    taxCode: string | null;
    taxableAmount: string | null;
    taxAmount: string | null;
    paymentTermSnapshotLines: NonNullable<Selectable<"PurchaseOrder">["paymentTermSnapshotLines"]>;
    lineCustomFields: Record<string, unknown>;
  }[] = [];

  for (const line of lines) {
    const {
      purchaseOrderLineId,
      quantity,
      billedUnitPrice,
      taxCode,
      taxableAmount,
      taxAmount,
      ...lineCustomFields
    } = line;
    if (new Decimal(quantity).lte(0)) {
      return err(new InvalidBilledQuantityError(purchaseOrderLineId ?? "unknown"));
    }

    if (purchaseOrderLineId == null) {
      // PO-unlinked bill line
      normalizedLines.push({
        purchaseOrderLineId: null,
        quantity,
        billedUnitPrice: billedUnitPrice ?? null,
        taxCode: taxCode ?? null,
        taxableAmount: taxableAmount ?? null,
        taxAmount: taxAmount ?? null,
        paymentTermSnapshotLines: [],
        lineCustomFields,
      });
      continue;
    }

    const orderLine = await db
      .selectFrom("PurchaseOrderLine")
      .selectAll()
      .where("id", "=", purchaseOrderLineId)
      .executeTakeFirst();
    if (!orderLine) {
      return err(new PurchaseOrderLineNotFoundError(purchaseOrderLineId));
    }

    const purchaseOrder = await db
      .selectFrom("PurchaseOrder")
      .selectAll()
      .where("id", "=", orderLine.purchaseOrderId)
      .executeTakeFirst();
    if (!purchaseOrder) {
      return err(new PurchaseOrderLineNotFoundError(purchaseOrderLineId));
    }
    if (purchaseOrder.orderStatus === "CANCELLED") {
      return err(new PurchaseOrderCancelledError(purchaseOrder.id));
    }

    normalizedLines.push({
      purchaseOrderLineId,
      quantity,
      billedUnitPrice: billedUnitPrice ?? orderLine.unitPrice,
      taxCode: taxCode ?? null,
      taxableAmount: taxableAmount ?? null,
      taxAmount: taxAmount ?? null,
      paymentTermSnapshotLines: purchaseOrder.paymentTermSnapshotLines ?? [],
      lineCustomFields,
    });
  }

  const poLinkedLines = normalizedLines.filter((l) => l.purchaseOrderLineId !== null);
  const firstPoLinkedLine = poLinkedLines[0];
  // Payment term snapshot is required only when PO-linked lines exist
  if (firstPoLinkedLine && !firstPoLinkedLine.paymentTermSnapshotLines.length) {
    return err(new PaymentTermSnapshotMissingError(invoiceNumber));
  }

  let supplierTaxIdentification: string | undefined = inputTaxId;
  if (!supplierTaxIdentification) {
    const idResult = await businessPartnerQueries.listPartnerIdentificationsByPartner(
      db,
      { partnerId: supplierId },
      ctx,
    );
    if (idResult.ok) {
      supplierTaxIdentification = idResult.value.items[0]?.value as string | undefined;
    }
  }
  if (!supplierTaxIdentification) {
    return err(new SupplierTaxIdentificationMissingError(supplierId));
  }

  const purchaseBill = await db
    .insertInto("PurchaseBill")
    .values({
      ...(customFields as Record<string, unknown>),
      companyId,
      supplierId,
      invoiceNumber,
      invoiceDate,
      status: "DRAFT",
      paymentTermSnapshotLines: firstPoLinkedLine?.paymentTermSnapshotLines ?? null,
      supplierTaxIdentification,
      matchingMode: null,
      varianceReason: null,
      releaseReason: null,
      dueSchedule: null,
      createdAt: new Date(),
      updatedAt: null,
    })
    .returningAll()
    .executeTakeFirst();

  await db
    .insertInto("PurchaseBillLine")
    .values(
      normalizedLines.map((line) => ({
        ...line.lineCustomFields,
        purchaseBillId: purchaseBill!.id,
        purchaseOrderLineId: line.purchaseOrderLineId,
        quantity: line.quantity,
        billedUnitPrice: line.billedUnitPrice,
        taxCode: line.taxCode,
        taxableAmount: line.taxableAmount,
        taxAmount: line.taxAmount,
        createdAt: new Date(),
        updatedAt: null,
      })),
    )
    .execute();

  return ok({ purchaseBill: purchaseBill! });
}
