import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import type { Selectable, Transaction, Updateable } from "../generated/kysely-tailordb";
import { encodeJson, type SalesOrderLineInput } from "../lib/domain";
import {
  CrossCompanyReferenceError,
  EmptyOrderNotAllowedError,
  InvalidOrderLineError,
  ItemNotActiveError,
  ItemNotFoundError,
  LineNotFoundError,
  OrderNotEditableError,
  SalesOrderNotFoundError,
} from "../lib/errors.generated";
import type { ItemManagementQueries } from "../module";

export interface SalesOrderHeaderPatch {
  customerId?: string;
  paymentTermId?: string | null;
  channelId?: string | null;
  requestedShipDate?: Date | null;
  shippingAddressSnapshot?: Record<string, unknown> | null;
  billingAddressSnapshot?: Record<string, unknown> | null;
}

export interface SalesOrderLinePatch {
  itemId?: string;
  description?: string | null;
  quantity?: number;
  unitPrice?: number;
  currencyId?: string;
  unitId?: string | null;
  matchedPriceRuleId?: string | null;
  billingPolicy?: "ORDER" | "SHIPMENT";
  requestedShipDate?: Date | null;
  requiresPhysicalFulfillment?: boolean;
  priceSnapshot?: Record<string, unknown> | null;
  shippedQuantity?: number;
  invoicedQuantity?: number;
}

export interface SalesOrderLineEdit {
  lineId: string;
  linePatch: SalesOrderLinePatch;
}

export type UpdateSalesOrderInput = {
  id: string;
  headerPatch?: SalesOrderHeaderPatch;
  addLines?: SalesOrderLineInput[];
  updateLines?: SalesOrderLineEdit[];
  removeLineIds?: string[];
};

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

function checkSalesOrderInvariants(id: string, lines: SalesOrderLineState[]) {
  if (lines.length === 0) {
    return new EmptyOrderNotAllowedError(id);
  }
  for (const line of lines) {
    if (!line.itemId || line.quantity <= 0 || line.unitPrice < 0 || !line.currencyId) {
      return new InvalidOrderLineError(line.itemId || id);
    }
  }
  return null;
}

export async function run<CF extends Record<string, unknown>, LCF extends Record<string, unknown>>(
  db: Transaction,
  input: Omit<UpdateSalesOrderInput, "headerPatch" | "addLines" | "updateLines"> & {
    headerPatch?: SalesOrderHeaderPatch & Partial<CF>;
    addLines?: (SalesOrderLineInput & LCF)[];
    updateLines?: { lineId: string; linePatch: SalesOrderLinePatch & Partial<LCF> }[];
  },
  ctx: CommandContext,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
) {
  const { id, headerPatch, addLines = [], updateLines = [], removeLineIds = [] } = input;

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

  // Index the current lines; reject any update or removal that targets a missing line.
  const currentLines = await db
    .selectFrom("SalesOrderLine")
    .selectAll()
    .where("salesOrderId", "=", 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 master data =====
  // Items referenced by added or re-pointed lines must exist, be ACTIVE, and share the company.
  const validatedItemIds = new Set<string>();
  const itemIdsToValidate = [
    ...addLines.map((line) => line.itemId),
    ...updateLines.flatMap((edit) =>
      edit.linePatch.itemId === undefined ? [] : [edit.linePatch.itemId],
    ),
  ];
  for (const itemId of itemIdsToValidate) {
    if (validatedItemIds.has(itemId)) {
      continue;
    }
    const { item } = (await itemManagementQueries.getItem(db, { id: itemId }, ctx)).value;
    if (!item) {
      return err(new ItemNotFoundError(itemId));
    }
    if (item.status !== "ACTIVE") {
      return err(new ItemNotActiveError(itemId));
    }
    const itemCompanyId = (item as { companyId?: string }).companyId;
    if (itemCompanyId && itemCompanyId !== currentHeader.companyId) {
      return err(new CrossCompanyReferenceError(itemId));
    }
    validatedItemIds.add(itemId);
  }

  // ===== 3. Reconstitute the amended order and check its invariants =====
  const mergedLines: SalesOrderLineState[] = [];
  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({
      itemId: linePatch.itemId === undefined ? line.itemId : linePatch.itemId,
      description: linePatch.description === undefined ? line.description : linePatch.description,
      quantity: linePatch.quantity === undefined ? line.quantity : linePatch.quantity,
      unitPrice: linePatch.unitPrice === undefined ? line.unitPrice : linePatch.unitPrice,
      currencyId: linePatch.currencyId === undefined ? line.currencyId : linePatch.currencyId,
      unitId: linePatch.unitId === undefined ? line.unitId : linePatch.unitId,
      matchedPriceRuleId:
        linePatch.matchedPriceRuleId === undefined
          ? line.matchedPriceRuleId
          : linePatch.matchedPriceRuleId,
      billingPolicy:
        linePatch.billingPolicy === undefined ? line.billingPolicy : linePatch.billingPolicy,
      requestedShipDate:
        linePatch.requestedShipDate === undefined
          ? line.requestedShipDate
          : linePatch.requestedShipDate,
      requiresPhysicalFulfillment:
        linePatch.requiresPhysicalFulfillment === undefined
          ? line.requiresPhysicalFulfillment
          : linePatch.requiresPhysicalFulfillment,
      priceSnapshot:
        linePatch.priceSnapshot === undefined
          ? line.priceSnapshot
          : linePatch.priceSnapshot
            ? encodeJson(linePatch.priceSnapshot)
            : null,
      shippedQuantity:
        linePatch.shippedQuantity === undefined ? line.shippedQuantity : linePatch.shippedQuantity,
      invoicedQuantity:
        linePatch.invoicedQuantity === undefined
          ? line.invoicedQuantity
          : linePatch.invoicedQuantity,
    });
  }
  for (const line of addLines) {
    mergedLines.push({
      itemId: line.itemId,
      description: line.description ?? null,
      quantity: line.quantity,
      unitPrice: line.unitPrice,
      currencyId: line.currencyId,
      unitId: line.unitId ?? null,
      matchedPriceRuleId: line.matchedPriceRuleId ?? null,
      billingPolicy: line.billingPolicy ?? "ORDER",
      requestedShipDate: line.requestedShipDate ?? null,
      requiresPhysicalFulfillment: line.requiresPhysicalFulfillment ?? true,
      priceSnapshot: line.priceSnapshot ? encodeJson(line.priceSnapshot) : null,
      shippedQuantity: line.shippedQuantity ?? 0,
      invoicedQuantity: line.invoicedQuantity ?? 0,
    });
  }

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

  // ===== 4. Apply the order changes =====
  // Header — write the patch, encoding the address snapshots as the create command does.
  let salesOrder = currentHeader;
  if (headerPatch) {
    const { shippingAddressSnapshot, billingAddressSnapshot, ...headerColumns } = headerPatch;
    const updates: Updateable<"SalesOrder"> = { ...headerColumns };
    if (shippingAddressSnapshot !== undefined) {
      updates.shippingAddressSnapshot = shippingAddressSnapshot
        ? encodeJson(shippingAddressSnapshot)
        : null;
    }
    if (billingAddressSnapshot !== undefined) {
      updates.billingAddressSnapshot = billingAddressSnapshot
        ? encodeJson(billingAddressSnapshot)
        : null;
    }
    // Skip an empty patch; kysely can't compile a SET with no columns.
    if (Object.values(updates).some((value) => value !== undefined)) {
      salesOrder = await db
        .updateTable("SalesOrder")
        .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 { priceSnapshot, ...lineColumns } = linePatch;
    const lineUpdates: Updateable<"SalesOrderLine"> = { ...lineColumns };
    if (priceSnapshot !== undefined) {
      lineUpdates.priceSnapshot = priceSnapshot ? encodeJson(priceSnapshot) : null;
    }
    // Skip an empty patch; kysely can't compile a SET with no columns.
    if (Object.values(lineUpdates).some((value) => value !== undefined)) {
      await db.updateTable("SalesOrderLine").set(lineUpdates).where("id", "=", lineId).execute();
    }
  }

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

  // Added lines — insert with command-set columns winning over custom fields.
  const addRows = addLines.map((line) => {
    return {
      // Raw line first (incl. custom fields) so command-set columns always win.
      ...(line as Record<string, unknown>),
      salesOrderId: id,
      itemId: line.itemId,
      description: line.description ?? null,
      quantity: line.quantity,
      unitPrice: line.unitPrice,
      currencyId: line.currencyId,
      unitId: line.unitId ?? null,
      matchedPriceRuleId: line.matchedPriceRuleId ?? null,
      billingPolicy: line.billingPolicy ?? "ORDER",
      requestedShipDate: line.requestedShipDate ?? null,
      requiresPhysicalFulfillment: line.requiresPhysicalFulfillment ?? true,
      priceSnapshot: line.priceSnapshot ? encodeJson(line.priceSnapshot) : null,
      shippedQuantity: line.shippedQuantity ?? 0,
      invoicedQuantity: line.invoicedQuantity ?? 0,
    };
  });
  if (addRows.length > 0) {
    await db.insertInto("SalesOrderLine").values(addRows).execute();
  }

  return ok({ salesOrder });
}
