import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import { inventoryExecutionLifecycle } from "../db/inventoryExecution.lifecycle.generated";
import type { Insertable, Selectable, Transaction, Updateable } from "../generated/kysely-tailordb";
import {
  EmptyIssueLinesError,
  InvalidQuantityError,
  InvalidStatusError,
  InvalidUnitConversionRateError,
  IssueLineNotFoundError,
  ItemNotFoundError,
  LotNotFoundError,
  PrimaryUnitMismatchError,
  SerialNotFoundError,
  StorageLocationNotFoundError,
} from "../lib/errors.generated";
import type { ItemManagementQueries } from "../module";
import type { GoodsIssueLineInput } from "./createGoodsIssue";

export interface GoodsIssueHeaderPatch {
  effectiveDate?: Date;
}

export interface GoodsIssueLinePatch {
  itemId?: string;
  storageLocationId?: string;
  quantity?: string;
  unitId?: string;
  primaryQuantity?: string;
  primaryUnitId?: string;
  unitConversionRate?: string;
  sourceLineId?: string | null;
  lotId?: string | null;
  serialNumberId?: string | null;
}

export interface GoodsIssueLineEdit {
  lineId: string;
  linePatch: GoodsIssueLinePatch;
}

export type UpdateGoodsIssueInput = {
  id: string;
  headerPatch?: GoodsIssueHeaderPatch;
  addLines?: GoodsIssueLineInput[];
  updateLines?: GoodsIssueLineEdit[];
  removeLineIds?: string[];
};

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

function checkGoodsIssueInvariants(id: string, lines: GoodsIssueLineState[]) {
  if (lines.length === 0) {
    return new EmptyIssueLinesError(id);
  }
  for (const line of lines) {
    if (new Decimal(line.quantity).lte(0)) {
      return new InvalidQuantityError(line.quantity);
    }
    if (new Decimal(line.primaryQuantity).lte(0)) {
      return new InvalidQuantityError(line.primaryQuantity);
    }
    if (new Decimal(line.unitConversionRate).lte(0)) {
      return new InvalidUnitConversionRateError(line.unitConversionRate);
    }
  }
  return null;
}

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

  // ===== 1. Load the current execution and its lines =====
  // Lock the execution row; only a DRAFT goods issue can be edited.
  const currentHeader = await db
    .selectFrom("InventoryExecution")
    .selectAll()
    .where("id", "=", id)
    .forUpdate()
    .executeTakeFirst();
  if (!currentHeader || currentHeader.executionType !== "GOODS_ISSUE") {
    return err(new InvalidStatusError(id));
  }
  if (!inventoryExecutionLifecycle.tryTransition(currentHeader.status, "update")) {
    return err(new InvalidStatusError(id));
  }

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

  // ===== 2. Validate the referenced master data =====
  // Items — each newly referenced item must exist and its primary unit must match.
  const itemUnitByItemId = new Map<string, string>();
  const itemIdsToValidate = [
    ...addLines.map((line) => line.itemId),
    ...updateLines.flatMap((edit) => {
      if (edit.linePatch.itemId !== undefined) {
        return [edit.linePatch.itemId];
      }
      // primaryUnitId-only change: still validate against the current line's item.
      if (edit.linePatch.primaryUnitId !== undefined) {
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        return [currentLineById.get(edit.lineId)!.itemId];
      }
      return [];
    }),
  ];
  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);
  }

  // Storage locations — every referenced location must exist.
  const locationIdsToValidate = new Set<string>();
  for (const line of addLines) {
    locationIdsToValidate.add(line.storageLocationId);
  }
  for (const { linePatch } of updateLines) {
    if (linePatch.storageLocationId !== undefined) {
      locationIdsToValidate.add(linePatch.storageLocationId);
    }
  }
  if (locationIdsToValidate.size > 0) {
    const existing = await db
      .selectFrom("StorageLocation")
      .select("id")
      .where("id", "in", [...locationIdsToValidate])
      .execute();
    const existingIds = new Set(existing.map((row) => row.id));
    for (const locationId of locationIdsToValidate) {
      if (!existingIds.has(locationId)) {
        return err(new StorageLocationNotFoundError(locationId));
      }
    }
  }

  // Lots — every referenced lot must exist.
  const lotIdsToValidate = new Set<string>();
  for (const line of addLines) {
    if (line.lotId != null) lotIdsToValidate.add(line.lotId);
  }
  for (const { linePatch } of updateLines) {
    if (linePatch.lotId != null) lotIdsToValidate.add(linePatch.lotId);
  }
  if (lotIdsToValidate.size > 0) {
    const existing = await db
      .selectFrom("Lot")
      .select("id")
      .where("id", "in", [...lotIdsToValidate])
      .execute();
    const existingIds = new Set(existing.map((row) => row.id));
    for (const lotId of lotIdsToValidate) {
      if (!existingIds.has(lotId)) {
        return err(new LotNotFoundError(lotId));
      }
    }
  }

  // Serial numbers — every referenced serial must exist.
  const serialIdsToValidate = new Set<string>();
  for (const line of addLines) {
    if (line.serialNumberId != null) serialIdsToValidate.add(line.serialNumberId);
  }
  for (const { linePatch } of updateLines) {
    if (linePatch.serialNumberId != null) serialIdsToValidate.add(linePatch.serialNumberId);
  }
  if (serialIdsToValidate.size > 0) {
    const existing = await db
      .selectFrom("SerialNumber")
      .select("id")
      .where("id", "in", [...serialIdsToValidate])
      .execute();
    const existingIds = new Set(existing.map((row) => row.id));
    for (const serialId of serialIdsToValidate) {
      if (!existingIds.has(serialId)) {
        return err(new SerialNotFoundError(serialId));
      }
    }
  }

  // primaryUnitId must equal the resolved item's primary unit, for adds and any line whose
  // itemId or primaryUnitId changed.
  for (const line of addLines) {
    if (line.primaryUnitId !== itemUnitByItemId.get(line.itemId)) {
      return err(new PrimaryUnitMismatchError(line.primaryUnitId));
    }
  }
  for (const { lineId, linePatch } of updateLines) {
    if (linePatch.itemId === undefined && linePatch.primaryUnitId === undefined) {
      continue;
    }
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const current = currentLineById.get(lineId)!;
    const itemId = linePatch.itemId ?? current.itemId;
    const primaryUnitId = linePatch.primaryUnitId ?? current.primaryUnitId;
    if (primaryUnitId !== itemUnitByItemId.get(itemId)) {
      return err(new PrimaryUnitMismatchError(primaryUnitId));
    }
  }

  // ===== 3. Reconstitute the amended execution and check its invariants =====
  const mergedLines: GoodsIssueLineState[] = [];
  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({
      direction: line.direction,
      itemId: linePatch.itemId === undefined ? line.itemId : linePatch.itemId,
      quantity: linePatch.quantity === undefined ? line.quantity : linePatch.quantity,
      unitId: linePatch.unitId === undefined ? line.unitId : linePatch.unitId,
      primaryQuantity:
        linePatch.primaryQuantity === undefined ? line.primaryQuantity : linePatch.primaryQuantity,
      primaryUnitId:
        linePatch.primaryUnitId === undefined ? line.primaryUnitId : linePatch.primaryUnitId,
      unitConversionRate:
        linePatch.unitConversionRate === undefined
          ? line.unitConversionRate
          : linePatch.unitConversionRate,
      unitCost: line.unitCost,
      storageLocationId:
        linePatch.storageLocationId === undefined
          ? line.storageLocationId
          : linePatch.storageLocationId,
      stockType: line.stockType,
      sourceLineId:
        linePatch.sourceLineId === undefined ? line.sourceLineId : linePatch.sourceLineId,
      lotId: linePatch.lotId === undefined ? line.lotId : linePatch.lotId,
      serialNumberId:
        linePatch.serialNumberId === undefined ? line.serialNumberId : linePatch.serialNumberId,
    });
  }
  for (const line of addLines) {
    mergedLines.push({
      direction: "OUT",
      itemId: line.itemId,
      quantity: line.quantity,
      unitId: line.unitId,
      primaryQuantity: line.primaryQuantity,
      primaryUnitId: line.primaryUnitId,
      unitConversionRate: line.unitConversionRate,
      unitCost: null,
      storageLocationId: line.storageLocationId,
      stockType: "AVAILABLE",
      sourceLineId: line.sourceLineId ?? null,
      lotId: line.lotId ?? null,
      serialNumberId: line.serialNumberId ?? null,
    });
  }

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

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

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

  // Added lines — insert.
  const addRows: Insertable<"InventoryExecutionLine">[] = addLines.map((line) => ({
    // Raw line first so command-set columns always win.
    ...(line as Record<string, unknown>),
    inventoryExecutionId: id,
    direction: "OUT",
    itemId: line.itemId,
    quantity: line.quantity,
    unitId: line.unitId,
    primaryQuantity: line.primaryQuantity,
    primaryUnitId: line.primaryUnitId,
    unitConversionRate: line.unitConversionRate,
    unitCost: null,
    storageLocationId: line.storageLocationId,
    stockType: "AVAILABLE",
    sourceLineId: line.sourceLineId ?? null,
    lotId: line.lotId ?? null,
    serialNumberId: line.serialNumberId ?? null,
  }));
  if (addRows.length > 0) {
    await db.insertInto("InventoryExecutionLine").values(addRows).execute();
  }

  return ok({ inventoryExecution });
}
