import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import type { Selectable, Transaction, Updateable } from "../generated/kysely-tailordb";
import { type SalesCreditNoteLineInput } from "../lib/domain";
import {
  CreditNoteNotEditableError,
  EmptySalesCreditNoteLinesError,
  InvalidCreditNoteLineError,
  LineNotFoundError,
  SalesCreditNoteNotFoundError,
} from "../lib/errors.generated";

export interface SalesCreditNoteHeaderPatch {
  reason?: string | null;
}

export interface SalesCreditNoteLinePatch {
  salesInvoiceLineId?: string;
  quantity?: number | null;
  amount?: number | null;
  reason?: string | null;
  taxAmount?: number | null;
}

export interface SalesCreditNoteLineEdit {
  lineId: string;
  linePatch: SalesCreditNoteLinePatch;
}

export type UpdateSalesCreditNoteInput = {
  id: string;
  headerPatch?: SalesCreditNoteHeaderPatch;
  addLines?: SalesCreditNoteLineInput[];
  updateLines?: SalesCreditNoteLineEdit[];
  removeLineIds?: string[];
};

// The credit note's domain-owned state that invariants are checked over, minus DB-assigned fields.
type SalesCreditNoteLineState = Omit<
  Selectable<"SalesCreditNoteLine">,
  "id" | "createdAt" | "updatedAt" | "salesCreditNoteId"
>;

function checkSalesCreditNoteInvariants(id: string, lines: SalesCreditNoteLineState[]) {
  if (lines.length === 0) {
    return new EmptySalesCreditNoteLinesError(id);
  }
  for (const line of lines) {
    if (!line.salesInvoiceLineId) {
      return new InvalidCreditNoteLineError(id);
    }
  }
  return null;
}

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

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

  // Index the current lines; reject any update or removal that targets a missing line.
  const currentLines = await db
    .selectFrom("SalesCreditNoteLine")
    .selectAll()
    .where("salesCreditNoteId", "=", 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. Reconstitute the amended credit note and check its invariants =====
  const mergedLines: SalesCreditNoteLineState[] = [];
  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({
      salesInvoiceLineId:
        linePatch.salesInvoiceLineId === undefined
          ? line.salesInvoiceLineId
          : linePatch.salesInvoiceLineId,
      quantity: linePatch.quantity === undefined ? line.quantity : linePatch.quantity,
      amount: linePatch.amount === undefined ? line.amount : linePatch.amount,
      reason: linePatch.reason === undefined ? line.reason : linePatch.reason,
      taxAmount: linePatch.taxAmount === undefined ? line.taxAmount : linePatch.taxAmount,
    });
  }
  for (const line of addLines) {
    mergedLines.push({
      salesInvoiceLineId: line.salesInvoiceLineId,
      quantity: line.quantity ?? null,
      amount: line.amount ?? null,
      reason: line.reason ?? null,
      taxAmount: line.taxAmount ?? null,
    });
  }

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

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

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

  // Added lines — insert with nullable defaults resolved.
  const addRows = addLines.map((line) => {
    return {
      // Raw line first (incl. custom fields) so command-set columns always win.
      ...(line as Record<string, unknown>),
      salesCreditNoteId: id,
      salesInvoiceLineId: line.salesInvoiceLineId,
      quantity: line.quantity ?? null,
      amount: line.amount ?? null,
      reason: line.reason ?? null,
      taxAmount: line.taxAmount ?? null,
    };
  });
  if (addRows.length > 0) {
    await db.insertInto("SalesCreditNoteLine").values(addRows).execute();
  }

  return ok({ salesCreditNote });
}
