import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import type {
  StockAdjustmentLineAdjustmentDirection,
  StockAdjustmentLineFromStockCategory,
} from "../generated/enums";
import type { Selectable, Transaction, Updateable } from "../generated/kysely-tailordb";
import {
  StockAdjustmentNotFoundError,
  StockAdjustmentLineNotFoundError,
  InvalidStatusError,
  EmptyAdjustmentLinesError,
  InvalidQuantityError,
  ItemNotFoundError,
  ItemNotActiveError,
  StorageLocationNotFoundError,
  StorageLocationNotActiveError,
  InvalidFromStockCategoryError,
  LotNotFoundError,
  SerialNotFoundError,
} from "../lib/errors.generated";
import type { ItemManagementQueries } from "../module";
import type { StockAdjustmentLineInput } from "./createStockAdjustment";

export interface StockAdjustmentHeaderPatch {
  reasonCode?: string;
  adjustmentDate?: Date;
}

export interface StockAdjustmentLinePatch {
  itemId?: string;
  storageLocationId?: string;
  quantity?: string;
  adjustmentDirection?: StockAdjustmentLineAdjustmentDirection | null;
  fromStockCategory?: StockAdjustmentLineFromStockCategory | null;
  lotId?: string | null;
  serialNumberId?: string | null;
  unitCost?: string | null;
}

export interface StockAdjustmentLineEdit {
  lineId: string;
  linePatch: StockAdjustmentLinePatch;
}

export type UpdateStockAdjustmentInput = {
  id: string;
  headerPatch?: StockAdjustmentHeaderPatch;
  addLines?: StockAdjustmentLineInput[];
  updateLines?: StockAdjustmentLineEdit[];
  removeLineIds?: string[];
};

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

function checkStockAdjustmentInvariants(
  id: string,
  adjustmentType: Selectable<"StockAdjustment">["adjustmentType"],
  lines: StockAdjustmentLineState[],
) {
  if (lines.length === 0) {
    return new EmptyAdjustmentLinesError(id);
  }
  for (const line of lines) {
    if (new Decimal(line.quantity).lte(0)) {
      return new InvalidQuantityError(line.quantity);
    }
    // fromStockCategory is only valid for SCRAP adjustments.
    if (adjustmentType !== "SCRAP" && line.fromStockCategory) {
      return new InvalidFromStockCategoryError(line.itemId);
    }
  }
  return null;
}

/**
 * Function: updateStockAdjustment
 *
 * Revises a stock adjustment in DRAFT or REJECTED status.
 * When updating a REJECTED adjustment, its status transitions back to DRAFT.
 */
export async function run<CF extends Record<string, unknown>, LCF extends Record<string, unknown>>(
  db: Transaction,
  input: Omit<UpdateStockAdjustmentInput, "headerPatch" | "addLines" | "updateLines"> & {
    headerPatch?: StockAdjustmentHeaderPatch & Partial<CF>;
    addLines?: (StockAdjustmentLineInput & LCF)[];
    updateLines?: { lineId: string; linePatch: StockAdjustmentLinePatch & Partial<LCF> }[];
  },
  ctx: CommandContext,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
) {
  const { id, headerPatch, addLines = [], updateLines = [], removeLineIds = [] } = input;

  // ===== 1. Load the current adjustment and its lines =====
  // Lock the adjustment row; only DRAFT or REJECTED adjustments can be edited.
  const currentHeader = await db
    .selectFrom("StockAdjustment")
    .selectAll()
    .where("id", "=", id)
    .forUpdate()
    .executeTakeFirst();
  if (!currentHeader) {
    return err(new StockAdjustmentNotFoundError(id));
  }
  if (currentHeader.status !== "DRAFT" && currentHeader.status !== "REJECTED") {
    return err(new InvalidStatusError(id));
  }

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

  // ===== 2. Validate the referenced master data =====
  // Items — each newly referenced item must exist and be ACTIVE.
  const itemIdsToValidate = [
    ...addLines.map((line) => line.itemId),
    ...updateLines.flatMap((edit) =>
      edit.linePatch.itemId === undefined ? [] : [edit.linePatch.itemId],
    ),
  ];
  const checkedItemIds = new Set<string>();
  for (const itemId of itemIdsToValidate) {
    if (checkedItemIds.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));
    }
    checkedItemIds.add(itemId);
  }

  // Storage locations — every referenced location must exist and be ACTIVE.
  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")
      .selectAll()
      .where("id", "in", [...locationIdsToValidate])
      .execute();
    const byId = new Map(existing.map((row) => [row.id, row]));
    for (const locationId of locationIdsToValidate) {
      const location = byId.get(locationId);
      if (!location) {
        return err(new StorageLocationNotFoundError(locationId));
      }
      if (location.status !== "ACTIVE") {
        return err(new StorageLocationNotActiveError(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));
      }
    }
  }

  // ===== 3. Reconstitute the amended adjustment and check its invariants =====
  const mergedLines: StockAdjustmentLineState[] = [];
  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,
      storageLocationId:
        linePatch.storageLocationId === undefined
          ? line.storageLocationId
          : linePatch.storageLocationId,
      quantity: linePatch.quantity === undefined ? line.quantity : linePatch.quantity,
      adjustmentDirection:
        linePatch.adjustmentDirection === undefined
          ? line.adjustmentDirection
          : linePatch.adjustmentDirection,
      fromStockCategory:
        linePatch.fromStockCategory === undefined
          ? line.fromStockCategory
          : linePatch.fromStockCategory,
      unitCost: linePatch.unitCost === undefined ? line.unitCost : linePatch.unitCost,
      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,
      storageLocationId: line.storageLocationId,
      quantity: line.quantity,
      adjustmentDirection: line.adjustmentDirection ?? null,
      fromStockCategory: line.fromStockCategory ?? null,
      unitCost: line.unitCost ?? null,
      lotId: line.lotId ?? null,
      serialNumberId: line.serialNumberId ?? null,
    });
  }

  const violation = checkStockAdjustmentInvariants(id, currentHeader.adjustmentType, mergedLines);
  if (violation) {
    return err(violation);
  }

  // ===== 4. Apply the adjustment changes =====
  // Header — write the patch; a REJECTED adjustment reverts to DRAFT and clears its reason.
  const updateData: Updateable<"StockAdjustment"> = {
    ...(headerPatch as Updateable<"StockAdjustment">),
  };
  if (currentHeader.status === "REJECTED") {
    updateData.status = "DRAFT";
    updateData.rejectionReason = null;
  }
  let stockAdjustment = currentHeader;
  // Skip an empty patch; kysely can't compile a SET with no columns.
  if (Object.values(updateData).some((value) => value !== undefined)) {
    stockAdjustment = await db
      .updateTable("StockAdjustment")
      .set(updateData)
      .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<"StockAdjustmentLine"> = {
      ...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("StockAdjustmentLine")
        .set(lineUpdates)
        .where("id", "=", lineId)
        .execute();
    }
  }

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

  // Added lines — insert.
  const addRows = addLines.map((line) => {
    return {
      // Raw line first so command-set columns always win.
      ...(line as Record<string, unknown>),
      stockAdjustmentId: id,
      itemId: line.itemId,
      storageLocationId: line.storageLocationId,
      quantity: line.quantity,
      adjustmentDirection: line.adjustmentDirection ?? null,
      fromStockCategory: line.fromStockCategory ?? null,
      unitCost: line.unitCost ?? null,
      lotId: line.lotId ?? null,
      serialNumberId: line.serialNumberId ?? null,
    };
  });
  if (addRows.length > 0) {
    await db.insertInto("StockAdjustmentLine").values(addRows).execute();
  }

  return ok({ stockAdjustment });
}
