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 {
  DescriptionRequiredError,
  EmptyPurchaseRequisitionLinesError,
  InvalidEstimatedUnitPriceError,
  InvalidQuantityError,
  ItemNotFoundError,
  LineNotFoundError,
  PartnerNotSupplierError,
  PurchaseRequisitionNotDraftError,
  PurchaseRequisitionNotFoundError,
  SupplierNotActiveError,
  SupplierNotFoundError,
} from "../lib/errors.generated";
import type { BusinessPartnerQueries, ItemManagementQueries } from "../module";
import type { PurchaseRequisitionLineInput } from "./createPurchaseRequisition";

export interface PurchaseRequisitionHeaderPatch {
  justification?: string | null;
  desiredDeliveryDate?: Date | null;
}

export interface PurchaseRequisitionLinePatch {
  description?: string;
  quantity?: string;
  estimatedUnitPrice?: string;
  itemId?: string;
  unitId?: string;
  suggestedSupplierId?: string | null;
  needByDate?: Date | null;
}

export interface PurchaseRequisitionLineEdit {
  lineId: string;
  linePatch: PurchaseRequisitionLinePatch;
}

export type UpdatePurchaseRequisitionInput = {
  id: string;
  headerPatch?: PurchaseRequisitionHeaderPatch;
  addLines?: PurchaseRequisitionLineInput[];
  updateLines?: PurchaseRequisitionLineEdit[];
  removeLineIds?: string[];
};

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

function checkPurchaseRequisitionInvariants(id: string, lines: PurchaseRequisitionLineState[]) {
  if (lines.length === 0) {
    return new EmptyPurchaseRequisitionLinesError(id);
  }
  for (const line of lines) {
    if (!line.description) {
      return new DescriptionRequiredError(String(line.description));
    }
    if (new Decimal(line.quantity).lte(0)) {
      return new InvalidQuantityError(line.description);
    }
    if (new Decimal(line.estimatedUnitPrice).lt(0)) {
      return new InvalidEstimatedUnitPriceError(line.description);
    }
  }
  return null;
}

export async function run<CF extends Record<string, unknown>, LCF extends Record<string, unknown>>(
  db: Transaction,
  input: Omit<UpdatePurchaseRequisitionInput, "headerPatch" | "addLines" | "updateLines"> & {
    headerPatch?: PurchaseRequisitionHeaderPatch & Partial<CF>;
    addLines?: (PurchaseRequisitionLineInput & LCF)[];
    updateLines?: { lineId: string; linePatch: PurchaseRequisitionLinePatch & Partial<LCF> }[];
  },
  ctx: CommandContext,
  businessPartnerQueries: Pick<BusinessPartnerQueries, "getPartner" | "getPartnerRole">,
  itemManagementQueries: Pick<ItemManagementQueries, "getItem">,
) {
  const { id, headerPatch, addLines = [], updateLines = [], removeLineIds = [] } = input;

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

  // Index the current lines; reject any update or removal that targets a missing line.
  const currentLines = await db
    .selectFrom("PurchaseRequisitionLine")
    .selectAll()
    .where("purchaseRequisitionId", "=", 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.
  // Items — every newly referenced item must exist.
  const itemIdsToValidate = new Set<string>();
  for (const line of addLines) {
    if (line.itemId) {
      itemIdsToValidate.add(line.itemId);
    }
  }
  for (const { linePatch } of updateLines) {
    if (linePatch.itemId) {
      itemIdsToValidate.add(linePatch.itemId);
    }
  }
  for (const itemId of itemIdsToValidate) {
    const { item } = (await itemManagementQueries.getItem(db, { id: itemId }, ctx)).value;
    if (!item) {
      return err(new ItemNotFoundError(itemId));
    }
  }

  // Suggested suppliers — every newly referenced supplier must exist, be ACTIVE, and carry the SUPPLIER role.
  const supplierIdsToValidate = new Set<string>();
  for (const line of addLines) {
    if (line.suggestedSupplierId) {
      supplierIdsToValidate.add(line.suggestedSupplierId);
    }
  }
  for (const { linePatch } of updateLines) {
    if (linePatch.suggestedSupplierId) {
      supplierIdsToValidate.add(linePatch.suggestedSupplierId);
    }
  }
  for (const supplierId of supplierIdsToValidate) {
    const { partner: supplier } = (
      await businessPartnerQueries.getPartner(db, { id: supplierId }, ctx)
    ).value;
    if (!supplier) {
      return err(new SupplierNotFoundError(supplierId));
    }
    if (supplier.status !== "ACTIVE") {
      return err(new SupplierNotActiveError(supplierId));
    }
    const { partnerRole } = (
      await businessPartnerQueries.getPartnerRole(
        db,
        { partnerId: supplierId, role: "SUPPLIER" },
        ctx,
      )
    ).value;
    if (!partnerRole) {
      return err(new PartnerNotSupplierError(supplierId));
    }
  }

  // ===== 3. Reconstitute the amended requisition and check its invariants =====
  const mergedLines: PurchaseRequisitionLineState[] = [];
  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({
      description: linePatch.description === undefined ? line.description : linePatch.description,
      quantity: linePatch.quantity === undefined ? line.quantity : linePatch.quantity,
      estimatedUnitPrice:
        linePatch.estimatedUnitPrice === undefined
          ? line.estimatedUnitPrice
          : linePatch.estimatedUnitPrice,
      itemId: linePatch.itemId === undefined ? line.itemId : linePatch.itemId,
      unitId: linePatch.unitId === undefined ? line.unitId : linePatch.unitId,
      suggestedSupplierId:
        linePatch.suggestedSupplierId === undefined
          ? line.suggestedSupplierId
          : linePatch.suggestedSupplierId,
      needByDate: linePatch.needByDate === undefined ? line.needByDate : linePatch.needByDate,
    });
  }
  for (const line of addLines) {
    mergedLines.push({
      description: line.description,
      quantity: line.quantity,
      estimatedUnitPrice: line.estimatedUnitPrice,
      itemId: line.itemId,
      unitId: line.unitId,
      suggestedSupplierId: line.suggestedSupplierId ?? null,
      needByDate: line.needByDate ?? null,
    });
  }

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

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

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

  // Added lines — insert.
  const addRows = addLines.map((line) => {
    return {
      // Raw input first so command-set columns always win.
      ...(line as Record<string, unknown>),
      purchaseRequisitionId: id,
      description: line.description,
      quantity: line.quantity,
      estimatedUnitPrice: line.estimatedUnitPrice,
      itemId: line.itemId,
      unitId: line.unitId,
      suggestedSupplierId: line.suggestedSupplierId ?? null,
      needByDate: line.needByDate ?? null,
    };
  });
  if (addRows.length > 0) {
    await db.insertInto("PurchaseRequisitionLine").values(addRows).execute();
  }

  return ok({ purchaseRequisition });
}
