import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import type { Selectable, Transaction, Updateable } from "../generated/kysely-tailordb";
import { isPositiveDecimal } from "../internal/transferOrderProgress";
import {
  EmptyTransferLinesError,
  InvalidQuantityError,
  InvalidStateTransitionError,
  InvalidTransferLocationsError,
  ItemNotFoundError,
  SiteNotFoundError,
  TransferOrderLineNotFoundError,
  TransferOrderNotFoundError,
} from "../lib/errors.generated";
import type { ItemManagementQueries, OrganizationQueries } from "../module";
import type { TransferOrderLineInput } from "./createTransferOrder";

export interface TransferOrderHeaderPatch {
  sourceSiteId?: string;
  destinationSiteId?: string;
  plannedShipmentDate?: Date;
  expectedReceiptDate?: Date;
}

export interface TransferOrderLinePatch {
  itemId?: string;
  orderedQuantity?: string;
  lotId?: string | null;
  serialNumberId?: string | null;
}

export interface TransferOrderLineEdit {
  lineId: string;
  linePatch: TransferOrderLinePatch;
}

export type UpdateTransferOrderInput = {
  id: string;
  headerPatch?: TransferOrderHeaderPatch;
  addLines?: TransferOrderLineInput[];
  updateLines?: TransferOrderLineEdit[];
  removeLineIds?: string[];
};

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

type TransferOrderLineState = Omit<
  Selectable<"TransferOrderLine">,
  "id" | "createdAt" | "updatedAt" | "transferOrderId"
>;

function checkTransferOrderInvariants(
  id: string,
  header: TransferOrderHeaderState,
  lines: TransferOrderLineState[],
) {
  // Source and destination sites must differ.
  if (header.sourceSiteId === header.destinationSiteId) {
    return new InvalidTransferLocationsError(id);
  }
  if (lines.length === 0) {
    return new EmptyTransferLinesError(id);
  }
  for (const line of lines) {
    if (!isPositiveDecimal(line.orderedQuantity)) {
      return new InvalidQuantityError(line.orderedQuantity);
    }
  }
  return null;
}

export async function run<CF extends object = object, LCF extends object = object>(
  db: Transaction,
  input: Omit<UpdateTransferOrderInput, "headerPatch" | "addLines" | "updateLines"> & {
    headerPatch?: TransferOrderHeaderPatch & Partial<CF>;
    addLines?: (TransferOrderLineInput & Partial<LCF>)[];
    updateLines?: { lineId: string; linePatch: TransferOrderLinePatch & 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 transfer order can be edited.
  const currentHeader = await db
    .selectFrom("TransferOrder")
    .selectAll()
    .where("id", "=", id)
    .forUpdate()
    .executeTakeFirst();
  if (!currentHeader) {
    return err(new TransferOrderNotFoundError(id));
  }
  if (currentHeader.status !== "DRAFT") {
    return err(new InvalidStateTransitionError(id));
  }

  // Index the current lines; reject any update or removal that targets a missing line.
  const currentLines = await db
    .selectFrom("TransferOrderLine")
    .selectAll()
    .where("transferOrderId", "=", 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 TransferOrderLineNotFoundError(lineId));
    }
  }

  // ===== 2. Validate the referenced master data =====
  // Sites — only a newly referenced site is re-validated (source ≠ destination is an invariant).
  const changedSiteIds = [headerPatch?.sourceSiteId, headerPatch?.destinationSiteId].filter(
    (v): v is string => v != null,
  );
  for (const siteId of changedSiteIds) {
    const { site } = (await organizationQueries.getSite(db, { id: siteId }, ctx)).value;
    if (!site) {
      return err(new SiteNotFoundError(siteId));
    }
  }

  // Items — each newly referenced item must exist; its primary unit becomes the line unit.
  const itemUnitByItemId = new Map<string, string>();
  const itemIdsToValidate = [
    ...addLines.map((line) => line.itemId),
    ...updateLines.flatMap((edit) =>
      edit.linePatch.itemId === undefined ? [] : [edit.linePatch.itemId],
    ),
  ];
  for (const itemId of itemIdsToValidate) {
    if (itemUnitByItemId.has(itemId)) {
      continue;
    }
    const { item } = (await itemManagementQueries.getItem(db, { id: itemId }, ctx)).value;
    if (!item) {
      return err(new ItemNotFoundError(itemId));
    }
    itemUnitByItemId.set(itemId, item.unitId);
  }
  // Every itemId validated above is in the map, so this lookup never misses.
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  const unitIdFor = (itemId: string) => itemUnitByItemId.get(itemId)!;

  // ===== 3. Reconstitute the amended order and check its invariants =====
  const mergedLines: TransferOrderLineState[] = [];
  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,
      unitId: linePatch.itemId === undefined ? line.unitId : unitIdFor(linePatch.itemId),
      orderedQuantity:
        linePatch.orderedQuantity === undefined ? line.orderedQuantity : linePatch.orderedQuantity,
      shippedQuantity: line.shippedQuantity,
      receivedQuantity: line.receivedQuantity,
      lotId: linePatch.lotId === undefined ? line.lotId : linePatch.lotId,
      serialNumberId:
        linePatch.serialNumberId === undefined ? line.serialNumberId : linePatch.serialNumberId,
    });
  }
  for (const line of addLines) {
    mergedLines.push({
      itemId: line.itemId,
      unitId: unitIdFor(line.itemId),
      orderedQuantity: line.orderedQuantity,
      shippedQuantity: "0",
      receivedQuantity: "0",
      lotId: line.lotId ?? null,
      serialNumberId: line.serialNumberId ?? null,
    });
  }

  const mergedHeader: TransferOrderHeaderState = {
    ...currentHeader,
    sourceSiteId: headerPatch?.sourceSiteId ?? currentHeader.sourceSiteId,
    destinationSiteId: headerPatch?.destinationSiteId ?? currentHeader.destinationSiteId,
    plannedShipmentDate: headerPatch?.plannedShipmentDate ?? currentHeader.plannedShipmentDate,
    expectedReceiptDate: headerPatch?.expectedReceiptDate ?? currentHeader.expectedReceiptDate,
  };
  const violation = checkTransferOrderInvariants(id, mergedHeader, mergedLines);
  if (violation) {
    return err(violation);
  }

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

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

  // Added lines — insert with item-derived primary units.
  const addRows = addLines.map((line) => {
    return {
      // Raw line first so command-set columns always win.
      ...(line as Record<string, unknown>),
      transferOrderId: id,
      itemId: line.itemId,
      unitId: unitIdFor(line.itemId),
      orderedQuantity: line.orderedQuantity,
      shippedQuantity: "0",
      receivedQuantity: "0",
      lotId: line.lotId ?? null,
      serialNumberId: line.serialNumberId ?? null,
    };
  });
  if (addRows.length > 0) {
    await db.insertInto("TransferOrderLine").values(addRows).execute();
  }

  return ok({ transferOrder });
}
