import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import type { Selectable, Transaction, Updateable } from "../generated/kysely-tailordb";
import { encodeJson, type ShipmentLineInput } from "../lib/domain";
import {
  EmptyShipmentLinesError,
  InvalidShipmentLineError,
  LineNotFoundError,
  ShipmentNotEditableError,
  ShipmentNotFoundError,
} from "../lib/errors.generated";

export interface ShipmentHeaderPatch {
  shipFromSiteId?: string | null;
  trackingNumber?: string | null;
  shippingAddressSnapshot?: Record<string, unknown> | null;
}

export interface ShipmentLinePatch {
  salesOrderLineId?: string;
  quantity?: number;
  unitId?: string | null;
  trackingReference?: string | null;
}

export interface ShipmentLineEdit {
  lineId: string;
  linePatch: ShipmentLinePatch;
}

export type UpdateShipmentInput = {
  id: string;
  headerPatch?: ShipmentHeaderPatch;
  addLines?: ShipmentLineInput[];
  updateLines?: ShipmentLineEdit[];
  removeLineIds?: string[];
};

// The shipment's domain-owned line state that invariants are checked over, minus DB-assigned fields.
type ShipmentLineState = Omit<
  Selectable<"ShipmentLine">,
  "id" | "createdAt" | "updatedAt" | "shipmentId"
>;

function checkShipmentInvariants(id: string, lines: ShipmentLineState[]) {
  if (lines.length === 0) {
    return new EmptyShipmentLinesError(id);
  }
  for (const line of lines) {
    if (line.quantity <= 0) {
      return new InvalidShipmentLineError(id);
    }
  }
  return null;
}

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

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

  // Index the current lines; reject any update or removal that targets a missing line.
  const currentLines = await db
    .selectFrom("ShipmentLine")
    .selectAll()
    .where("shipmentId", "=", 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 =====
  const linesToValidate = [
    ...addLines.map((line) => ({
      salesOrderLineId: line.salesOrderLineId,
      quantity: line.quantity,
    })),
    ...updateLines.flatMap(({ lineId, linePatch }) => {
      if (linePatch.salesOrderLineId === undefined && linePatch.quantity === undefined) {
        return [];
      }
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      const currentLine = currentLineById.get(lineId)!;
      return [
        {
          salesOrderLineId: linePatch.salesOrderLineId ?? currentLine.salesOrderLineId,
          quantity: linePatch.quantity ?? currentLine.quantity,
        },
      ];
    }),
  ];

  const referencedOrderLineIds = [...new Set(linesToValidate.map((line) => line.salesOrderLineId))];
  if (referencedOrderLineIds.length > 0) {
    const orderLines = await db
      .selectFrom("SalesOrderLine")
      .selectAll()
      .where("id", "in", referencedOrderLineIds)
      .execute();
    const orderLineById = new Map(orderLines.map((orderLine) => [orderLine.id, orderLine]));
    for (const { salesOrderLineId, quantity } of linesToValidate) {
      const orderLine = orderLineById.get(salesOrderLineId);
      if (!orderLine || orderLine.salesOrderId !== currentHeader.salesOrderId) {
        return err(new InvalidShipmentLineError(id));
      }
      if (orderLine.quantity - orderLine.shippedQuantity < quantity) {
        return err(new InvalidShipmentLineError(id));
      }
    }
  }

  // ===== 3. Reconstitute the amended shipment and check its invariants =====
  const mergedLines: ShipmentLineState[] = [];
  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,
      quantity: linePatch.quantity === undefined ? line.quantity : linePatch.quantity,
      unitId: linePatch.unitId === undefined ? line.unitId : linePatch.unitId,
      trackingReference:
        linePatch.trackingReference === undefined
          ? line.trackingReference
          : linePatch.trackingReference,
    });
  }
  for (const line of addLines) {
    mergedLines.push({
      salesOrderLineId: line.salesOrderLineId,
      quantity: line.quantity,
      unitId: line.unitId ?? null,
      trackingReference: line.trackingReference ?? null,
    });
  }

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

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

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

  // Added lines — insert.
  const addRows = addLines.map((line) => {
    return {
      // Raw line first (incl. custom fields) so command-set columns always win.
      ...(line as Record<string, unknown>),
      shipmentId: id,
      salesOrderLineId: line.salesOrderLineId,
      quantity: line.quantity,
      unitId: line.unitId ?? null,
      trackingReference: line.trackingReference ?? null,
    };
  });
  if (addRows.length > 0) {
    await db.insertInto("ShipmentLine").values(addRows).execute();
  }

  return ok({ shipment });
}
