import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core";
import { Decimal } from "decimal.js";
import type { Transaction, Updateable } from "../generated/kysely-tailordb";
import {
  JournalEntryNotFoundError,
  InvalidStatusForUpdateError,
  AccountNotFoundError,
  AccountInactiveError,
  InvalidDebitCreditError,
  MinimumLinesNotMetError,
  EmptyJournalEntryChangesError,
  JournalLineNotFoundError,
  InvalidEntryDateError,
} from "../lib/errors.generated";
import type { CoaManagementQueries } from "../module";

export type JournalEntryHeaderPatch = {
  description?: string | null;
  sourceDocumentType?:
    | "ACCOUNT_PAYABLE_DOCUMENT"
    | "PURCHASE_BILL"
    | "SALES_INVOICE"
    | "INVENTORY_EXECUTION"
    | "PRODUCTION_ORDER"
    | "YEAR_END_CLOSE"
    | null;
  sourceDocumentId?: string | null;
  entryDate?: Date;
};

export interface JournalLineInput {
  accountId: string;
  debitAmount: string | null;
  creditAmount: string | null;
  description: string | null;
}

export type JournalLinePatch = Partial<JournalLineInput>;

export interface JournalLineEdit {
  lineId: string;
  patch: JournalLinePatch;
}

export type UpdateJournalEntryInput = {
  id: string;
  headerPatch?: JournalEntryHeaderPatch;
  addLines?: JournalLineInput[];
  updateLines?: JournalLineEdit[];
  removeLineIds?: string[];
};

function validateDebitCredit(
  identifier: string,
  debitAmount: string | null,
  creditAmount: string | null,
) {
  if (debitAmount != null && creditAmount != null) {
    return err(new InvalidDebitCreditError(identifier));
  }

  if (debitAmount == null && creditAmount == null) {
    return err(new InvalidDebitCreditError(identifier));
  }

  if (debitAmount != null && new Decimal(debitAmount).lte(0)) {
    return err(new InvalidDebitCreditError(identifier));
  }

  if (creditAmount != null && new Decimal(creditAmount).lte(0)) {
    return err(new InvalidDebitCreditError(identifier));
  }

  return ok({});
}

/**
 * Function: updateJournalEntry
 *
 * Replaces the editable draft journal document: mutable header fields plus all
 * prepared lines. Cancelled and posted entries are immutable.
 */
export async function run<HCF extends object = object, LCF extends object = object>(
  db: Transaction,
  input: Omit<UpdateJournalEntryInput, "headerPatch" | "addLines" | "updateLines"> & {
    headerPatch?: JournalEntryHeaderPatch & Partial<HCF>;
    addLines?: (JournalLineInput & LCF)[];
    updateLines?: (Omit<JournalLineEdit, "patch"> & {
      patch: JournalLinePatch & Partial<LCF>;
    })[];
  },
  ctx: CommandContext,
  coaManagementQueries: Pick<CoaManagementQueries, "listAccounts">,
) {
  const { id, headerPatch, addLines = [], updateLines = [], removeLineIds = [] } = input;
  const headerChangeCount = Object.keys(headerPatch ?? {}).length;
  const lineChangeCount = addLines.length + updateLines.length + removeLineIds.length;

  const existing = await db
    .selectFrom("JournalEntry")
    .selectAll()
    .where("id", "=", id)
    .forUpdate()
    .executeTakeFirst();

  if (!existing) {
    return err(new JournalEntryNotFoundError(id));
  }

  if (existing.status !== "DRAFT") {
    return err(new InvalidStatusForUpdateError(id));
  }

  if (headerChangeCount === 0 && lineChangeCount === 0) {
    return err(new EmptyJournalEntryChangesError(id));
  }

  if (
    headerPatch?.entryDate !== undefined &&
    (!(headerPatch.entryDate instanceof Date) || isNaN(headerPatch.entryDate.getTime()))
  ) {
    return err(new InvalidEntryDateError(String(headerPatch.entryDate)));
  }

  const currentLines = await db
    .selectFrom("JournalLine")
    .selectAll()
    .where("journalEntryId", "=", id)
    .execute();
  const currentLineById = new Map(currentLines.map((line) => [line.id, line]));

  for (const { lineId } of updateLines) {
    if (!currentLineById.has(lineId)) {
      return err(new JournalLineNotFoundError(lineId));
    }
  }

  for (const lineId of removeLineIds) {
    if (!currentLineById.has(lineId)) {
      return err(new JournalLineNotFoundError(lineId));
    }
  }

  const removeLineIdSet = new Set(removeLineIds);
  const updateLineById = new Map(updateLines.map((edit) => [edit.lineId, edit.patch]));
  const effectiveLines = [
    ...currentLines
      .filter((line) => !removeLineIdSet.has(line.id))
      .map((line) => {
        const patch = updateLineById.get(line.id);
        return patch ? { ...line, ...patch } : line;
      }),
    ...addLines,
  ];

  if (effectiveLines.length < 2) {
    return err(new MinimumLinesNotMetError(id));
  }

  const accountIds = [...new Set(effectiveLines.map((line) => line.accountId))];
  const accounts = (
    await coaManagementQueries.listAccounts(
      db,
      { companyId: existing.companyId, accountIds, limit: accountIds.length },
      ctx,
    )
  ).value.items;
  const accountById = new Map(accounts.map((account) => [account.id, account]));

  for (const line of effectiveLines) {
    const account = accountById.get(line.accountId);
    if (!account) {
      return err(new AccountNotFoundError(line.accountId));
    }
    if (account.status !== "ACTIVE") {
      return err(new AccountInactiveError(line.accountId));
    }

    const debitCreditResult = validateDebitCredit(id, line.debitAmount, line.creditAmount);
    if (!debitCreditResult.ok) return debitCreditResult;
  }

  const { description, sourceDocumentType, sourceDocumentId, entryDate, ...headerCustomFields } =
    headerPatch ?? {};
  const updateData: Updateable<"JournalEntry"> = {
    ...(headerCustomFields as Updateable<"JournalEntry">),
  };
  if (description !== undefined) updateData.description = description;
  if (sourceDocumentType !== undefined) updateData.sourceDocumentType = sourceDocumentType;
  if (sourceDocumentId !== undefined) updateData.sourceDocumentId = sourceDocumentId;
  if (entryDate !== undefined) updateData.entryDate = entryDate;

  const journalEntry =
    Object.keys(updateData).length === 0
      ? existing
      : await db
          .updateTable("JournalEntry")
          .set(updateData)
          .where("id", "=", id)
          .returningAll()
          .executeTakeFirstOrThrow();

  for (const { lineId, patch } of updateLines) {
    if (Object.keys(patch).length === 0) continue;
    await db
      .updateTable("JournalLine")
      .set(patch as Updateable<"JournalLine">)
      .where("id", "=", lineId)
      .execute();
  }

  if (removeLineIds.length > 0) {
    await db.deleteFrom("JournalLine").where("id", "in", removeLineIds).execute();
  }

  if (addLines.length > 0) {
    await db
      .insertInto("JournalLine")
      .values(
        addLines.map((line) => {
          const { accountId, debitAmount, creditAmount, description, ...lineCustomFields } = line;
          return {
            ...lineCustomFields,
            journalEntryId: id,
            accountId,
            debitAmount,
            creditAmount,
            description,
          };
        }),
      )
      .execute();
  }

  return ok({ journalEntry });
}
