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

export interface PurchaseOrderHeaderPatch {
  orderDate?: Date;
  receivingSiteId?: string | null;
  externalSupplierOrderReference?: string | null;
}

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

export interface PurchaseOrderLineEdit {
  lineId: string;
  linePatch: PurchaseOrderLinePatch;
}

export type UpdatePurchaseOrderInput = {
  id: string;
  headerPatch?: PurchaseOrderHeaderPatch;
  addLines?: PurchaseOrderLineInput[];
  updateLines?: PurchaseOrderLineEdit[];
  removeLineIds?: string[];
};

// 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: Omit<UpdatePurchaseOrderInput, "headerPatch" | "addLines" | "updateLines"> & {
    headerPatch?: PurchaseOrderHeaderPatch & Partial<CF>;
    addLines?: (PurchaseOrderLineInput & LCF)[];
    updateLines?: { lineId: string; linePatch: PurchaseOrderLinePatch & Partial<LCF> }[];
  },
  ctx: CommandContext,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
  organizationQueries: Pick<OrganizationQueries, "getSite">,
) {
  const { id, headerPatch, addLines = [], updateLines = [], removeLineIds = [] } = input;

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

  // Index the current lines; reject any update or removal that targets a missing line.
  const currentLines = await db
    .selectFrom("PurchaseOrderLine")
    .selectAll()
    .where("purchaseOrderId", "=", 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 =====
  // Each referenced entity is checked only where it is (re)chosen, and snapshotted when kept.
  // Items — must exist and be ACTIVE; snapshot its name and SKU.
  const itemSnapshotByItemId = new Map<string, { name: string; sku: string }>();
  const itemIdsToValidate = [
    ...addLines.map((line) => line.itemId),
    ...updateLines.flatMap((edit) =>
      edit.linePatch.itemId === undefined ? [] : [edit.linePatch.itemId],
    ),
  ];
  for (const itemId of itemIdsToValidate) {
    if (itemSnapshotByItemId.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));
    }
    itemSnapshotByItemId.set(itemId, { name: item.name, sku: item.sku });
  }
  // Every itemId 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 (headerPatch?.receivingSiteId != null) {
    siteIdsToValidate.add(headerPatch.receivingSiteId);
  }
  for (const line of addLines) {
    if (line.receivingSiteId != null) {
      siteIdsToValidate.add(line.receivingSiteId);
    }
  }
  for (const { linePatch } of updateLines) {
    if (linePatch.receivingSiteId != null) {
      siteIdsToValidate.add(linePatch.receivingSiteId);
    }
  }
  for (const siteId of siteIdsToValidate) {
    const { site } = (await organizationQueries.getSite(db, { id: siteId }, ctx)).value;
    if (!site) {
      return err(new ReceivingSiteNotFoundError(siteId));
    }
  }

  // ===== 3. Reconstitute the amended order and check its invariants =====
  const mergedLines: PurchaseOrderLineState[] = [];
  for (const line of currentLines) {
    if (removeLineIds.includes(line.id)) {
      continue;
    }
    const linePatch = linePatchById.get(line.id);
    if (!linePatch) {
      mergedLines.push(line);
      continue;
    }
    const itemSnapshot =
      linePatch.itemId === undefined ? undefined : itemSnapshotFor(linePatch.itemId);
    mergedLines.push({
      itemId: linePatch.itemId === undefined ? line.itemId : linePatch.itemId,
      itemSnapshotName: itemSnapshot === undefined ? line.itemSnapshotName : itemSnapshot.name,
      itemSnapshotSku: itemSnapshot === undefined ? line.itemSnapshotSku : itemSnapshot.sku,
      quantity: linePatch.quantity === undefined ? line.quantity : linePatch.quantity,
      unitPrice: linePatch.unitPrice === undefined ? line.unitPrice : linePatch.unitPrice,
      unitId: linePatch.unitId === undefined ? line.unitId : linePatch.unitId,
      receivingSiteId:
        linePatch.receivingSiteId === undefined ? line.receivingSiteId : linePatch.receivingSiteId,
      requiresPhysicalReceipt:
        linePatch.requiresPhysicalReceipt === undefined
          ? line.requiresPhysicalReceipt
          : linePatch.requiresPhysicalReceipt,
      billedQuantity: line.billedQuantity,
    });
  }
  for (const line of addLines) {
    const itemSnapshot = itemSnapshotFor(line.itemId);
    mergedLines.push({
      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 = {
    ...currentHeader,
    orderDate:
      headerPatch?.orderDate === undefined ? currentHeader.orderDate : headerPatch.orderDate,
    receivingSiteId:
      headerPatch?.receivingSiteId === undefined
        ? currentHeader.receivingSiteId
        : headerPatch.receivingSiteId,
    externalSupplierOrderReference:
      headerPatch?.externalSupplierOrderReference === undefined
        ? currentHeader.externalSupplierOrderReference
        : headerPatch.externalSupplierOrderReference,
  };

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

  // ===== 4. Apply the order changes =====
  // Header — write the patch.
  let purchaseOrder = currentHeader;
  if (headerPatch) {
    const updates: Updateable<"PurchaseOrder"> = {
      ...headerPatch,
    };
    // Skip an empty patch; kysely can't compile a SET with no columns.
    if (Object.values(updates).some((value) => value !== undefined)) {
      purchaseOrder = await db
        .updateTable("PurchaseOrder")
        .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<"PurchaseOrderLine"> = {
      ...linePatch,
    };
    if (linePatch.itemId !== undefined) {
      const itemSnapshot = itemSnapshotFor(linePatch.itemId);
      lineUpdates.itemSnapshotName = itemSnapshot.name;
      lineUpdates.itemSnapshotSku = itemSnapshot.sku;
    }
    // Skip an empty patch; kysely can't compile a SET with no columns.
    if (Object.values(lineUpdates).some((value) => value !== undefined)) {
      await db.updateTable("PurchaseOrderLine").set(lineUpdates).where("id", "=", lineId).execute();
    }
  }

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

  // Added lines — insert with the resolved item snapshots.
  const addRows = addLines.map((line) => {
    const {
      itemId,
      quantity,
      unitPrice,
      unitId,
      receivingSiteId,
      requiresPhysicalReceipt,
      ...lineCustomFields
    } = line;
    const itemSnapshot = itemSnapshotFor(itemId);
    return {
      // Custom fields first so command-set columns (purchaseOrderId, snapshots) always win.
      ...(lineCustomFields as Record<string, unknown>),
      purchaseOrderId: id,
      itemId,
      itemSnapshotName: itemSnapshot.name,
      itemSnapshotSku: itemSnapshot.sku,
      quantity,
      unitPrice,
      unitId,
      receivingSiteId,
      requiresPhysicalReceipt: requiresPhysicalReceipt ?? true,
    };
  });
  if (addRows.length > 0) {
    await db.insertInto("PurchaseOrderLine").values(addRows).execute();
  }

  return ok({ purchaseOrder });
}
