import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import type { Selectable, Transaction, Updateable } from "../generated/kysely-tailordb";
import { encodeJson, type SalesInvoiceLineInput } from "../lib/domain";
import {
  EmptySalesInvoiceLinesError,
  InvalidInvoiceLineError,
  InvoiceNotEditableError,
  LineNotFoundError,
  SalesInvoiceNotFoundError,
} from "../lib/errors.generated";

export interface SalesInvoiceHeaderPatch {
  invoiceDate?: Date;
  notes?: string | null;
}

export interface SalesInvoiceLinePatch {
  salesOrderLineId?: string;
  shipmentLineId?: string | null;
  quantity?: number;
  unitPrice?: number;
  taxCode?: string | null;
  taxableAmount?: number | null;
  taxAmount?: number | null;
  billingPolicy?: "ORDER" | "SHIPMENT";
}

export interface SalesInvoiceLineEdit {
  lineId: string;
  linePatch: SalesInvoiceLinePatch;
}

export type UpdateSalesInvoiceInput = {
  id: string;
  headerPatch?: SalesInvoiceHeaderPatch;
  addLines?: SalesInvoiceLineInput[];
  updateLines?: SalesInvoiceLineEdit[];
  removeLineIds?: string[];
};

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

function checkSalesInvoiceInvariants(id: string, lines: SalesInvoiceLineState[]) {
  if (lines.length === 0) {
    return new EmptySalesInvoiceLinesError(id);
  }
  for (const line of lines) {
    if (!line.salesOrderLineId || line.quantity <= 0) {
      return new InvalidInvoiceLineError(id);
    }
  }
  return null;
}

export async function run<CF extends Record<string, unknown>, LCF extends Record<string, unknown>>(
  db: Transaction,
  input: Omit<UpdateSalesInvoiceInput, "headerPatch" | "addLines" | "updateLines"> & {
    headerPatch?: SalesInvoiceHeaderPatch & Partial<CF>;
    addLines?: (SalesInvoiceLineInput & LCF)[];
    updateLines?: { lineId: string; linePatch: SalesInvoiceLinePatch & Partial<LCF> }[];
  },
  ctx: CommandContext,
) {
  void ctx;
  const { id, headerPatch, addLines = [], updateLines = [], removeLineIds = [] } = input;

  // ===== 1. Load the current invoice and its lines =====
  // Lock the invoice row; only a DRAFT sales invoice can be edited.
  const currentHeader = await db
    .selectFrom("SalesInvoice")
    .selectAll()
    .where("id", "=", id)
    .forUpdate()
    .executeTakeFirst();
  if (!currentHeader) {
    return err(new SalesInvoiceNotFoundError(id));
  }
  if (currentHeader.status !== "DRAFT") {
    return err(new InvoiceNotEditableError(id));
  }

  // Index the current lines; reject any update or removal that targets a missing line.
  const currentLines = await db
    .selectFrom("SalesInvoiceLine")
    .selectAll()
    .where("salesInvoiceId", "=", id)
    .execute();
  const currentLineById = new Map(currentLines.map((line) => [line.id, line]));
  const linePatchById = new Map(updateLines.map((edit) => [edit.lineId, edit.linePatch]));
  for (const lineId of [...linePatchById.keys(), ...removeLineIds]) {
    if (!currentLineById.has(lineId)) {
      return err(new LineNotFoundError(lineId));
    }
  }

  // ===== 2. Validate the referenced sales-order lines =====
  // Every newly referenced sales-order line must exist.
  const referencedOrderLineIds = [
    ...new Set(
      [
        ...addLines.map((line) => line.salesOrderLineId),
        ...updateLines.map((edit) => edit.linePatch.salesOrderLineId),
      ].filter((v): v is string => v !== undefined),
    ),
  ];
  const orderLineById = new Map<string, Selectable<"SalesOrderLine">>();
  if (referencedOrderLineIds.length > 0) {
    const orderLines = await db
      .selectFrom("SalesOrderLine")
      .selectAll()
      .where("id", "in", referencedOrderLineIds)
      .execute();
    for (const orderLine of orderLines) {
      orderLineById.set(orderLine.id, orderLine);
    }
    for (const orderLineId of referencedOrderLineIds) {
      if (!orderLineById.has(orderLineId)) {
        return err(new InvalidInvoiceLineError(orderLineId));
      }
    }
  }

  // ===== 3. Reconstitute the amended invoice and check its invariants =====
  const mergedLines: SalesInvoiceLineState[] = [];
  for (const line of currentLines) {
    if (removeLineIds.includes(line.id)) {
      continue;
    }
    const linePatch = linePatchById.get(line.id);
    if (!linePatch) {
      mergedLines.push(line);
      continue;
    }
    mergedLines.push({
      salesOrderLineId:
        linePatch.salesOrderLineId === undefined
          ? line.salesOrderLineId
          : linePatch.salesOrderLineId,
      shipmentLineId:
        linePatch.shipmentLineId === undefined ? line.shipmentLineId : linePatch.shipmentLineId,
      quantity: linePatch.quantity === undefined ? line.quantity : linePatch.quantity,
      unitPrice: linePatch.unitPrice === undefined ? line.unitPrice : linePatch.unitPrice,
      taxCode: linePatch.taxCode === undefined ? line.taxCode : linePatch.taxCode,
      taxableAmount:
        linePatch.taxableAmount === undefined ? line.taxableAmount : linePatch.taxableAmount,
      taxAmount: linePatch.taxAmount === undefined ? line.taxAmount : linePatch.taxAmount,
      billingPolicy:
        linePatch.billingPolicy === undefined ? line.billingPolicy : linePatch.billingPolicy,
      orderPriceSnapshot:
        linePatch.salesOrderLineId === undefined
          ? line.orderPriceSnapshot
          : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            encodeJson({ unitPrice: orderLineById.get(linePatch.salesOrderLineId)!.unitPrice }),
    });
  }
  for (const line of addLines) {
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const orderLine = orderLineById.get(line.salesOrderLineId)!;
    mergedLines.push({
      salesOrderLineId: line.salesOrderLineId,
      shipmentLineId: line.shipmentLineId ?? null,
      quantity: line.quantity,
      unitPrice: line.unitPrice ?? orderLine.unitPrice,
      taxCode: line.taxCode ?? null,
      taxableAmount: line.taxableAmount ?? null,
      taxAmount: line.taxAmount ?? null,
      billingPolicy:
        line.billingPolicy ?? (line.shipmentLineId ? "SHIPMENT" : orderLine.billingPolicy),
      orderPriceSnapshot: encodeJson({ unitPrice: orderLine.unitPrice }),
    });
  }

  const violation = checkSalesInvoiceInvariants(id, mergedLines);
  if (violation) {
    return err(violation);
  }

  // ===== 4. Apply the invoice changes =====
  // Header — write the patch.
  let salesInvoice = currentHeader;
  if (headerPatch) {
    const updates: Updateable<"SalesInvoice"> = {
      ...headerPatch,
    };
    // Skip an empty patch; kysely can't compile a SET with no columns.
    if (Object.values(updates).some((value) => value !== undefined)) {
      salesInvoice = await db
        .updateTable("SalesInvoice")
        .set(updates)
        .where("id", "=", id)
        .returningAll()
        .executeTakeFirstOrThrow();
    }
  }

  // Updated lines — patch in place so their ids and createdAt stay stable.
  for (const { lineId, linePatch } of updateLines) {
    const lineUpdates: Updateable<"SalesInvoiceLine"> = {
      ...linePatch,
    };
    if (linePatch.salesOrderLineId !== undefined) {
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      const orderLine = orderLineById.get(linePatch.salesOrderLineId)!;
      lineUpdates.orderPriceSnapshot = encodeJson({ unitPrice: orderLine.unitPrice });
    }
    // Skip an empty patch; kysely can't compile a SET with no columns.
    if (Object.values(lineUpdates).some((value) => value !== undefined)) {
      await db.updateTable("SalesInvoiceLine").set(lineUpdates).where("id", "=", lineId).execute();
    }
  }

  // Removed lines — delete.
  if (removeLineIds.length > 0) {
    await db.deleteFrom("SalesInvoiceLine").where("id", "in", removeLineIds).execute();
  }

  // Added lines — insert with sales-order-derived defaults resolved.
  const addRows = addLines.map((line) => {
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const orderLine = orderLineById.get(line.salesOrderLineId)!;
    return {
      // Raw line first (incl. custom fields) so command-set columns always win.
      ...(line as Record<string, unknown>),
      salesInvoiceId: id,
      salesOrderLineId: line.salesOrderLineId,
      shipmentLineId: line.shipmentLineId ?? null,
      quantity: line.quantity,
      unitPrice: line.unitPrice ?? orderLine.unitPrice,
      taxCode: line.taxCode ?? null,
      taxableAmount: line.taxableAmount ?? null,
      taxAmount: line.taxAmount ?? null,
      billingPolicy:
        line.billingPolicy ?? (line.shipmentLineId ? "SHIPMENT" : orderLine.billingPolicy),
      orderPriceSnapshot: encodeJson({ unitPrice: orderLine.unitPrice }),
    };
  });
  if (addRows.length > 0) {
    await db.insertInto("SalesInvoiceLine").values(addRows).execute();
  }

  return ok({ salesInvoice });
}
