import type { Transaction } from "../generated/kysely-tailordb";
import {
  AccountNotFoundError,
  ChartOfAccountsArchivedError,
  NameRequiredError,
  InvalidAccountCodeError,
  DuplicateAccountCodeError,
  StructuralFieldImmutableError,
  IncompatibleClassificationError,
  DuplicateRetainedEarningsError,
  ReconciliationRequiredError,
  CurrencyNotFoundError,
  AccountGroupNotFoundError,
  CodeOutOfRangeError,
} from "../lib/errors.generated";
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";

export type UpdateAccountInput = {
  id: string;
  name?: string;
  code?: string;
  accountType?: "ASSET" | "LIABILITY" | "EQUITY" | "REVENUE" | "EXPENSE";
  classification?:
    | "REGULAR"
    | "RECEIVABLE"
    | "PAYABLE"
    | "BANK_CASH"
    | "FIXED_ASSET"
    | "RETAINED_EARNINGS"
    | "NON_POSTING";
  reconciliation?: boolean;
  currencyRestriction?: string | null;
  defaultTaxCode?: string | null;
  tags?: string | null;
  accountGroupId?: string | null;
};

interface GetCurrencyDep {
  getCurrency: (
    db: Transaction,
    input: { id: string },
    ctx: CommandContext,
  ) => Promise<{ ok: true; value: { currency: { id: string; isActive: boolean } | null } }>;
}

const CLASSIFICATION_TYPE_MAP: Record<string, string[]> = {
  RECEIVABLE: ["ASSET"],
  BANK_CASH: ["ASSET"],
  FIXED_ASSET: ["ASSET"],
  PAYABLE: ["LIABILITY"],
  RETAINED_EARNINGS: ["EQUITY"],
};

const RECONCILIATION_REQUIRED_CLASSIFICATIONS = ["RECEIVABLE", "PAYABLE", "BANK_CASH"];

/**
 * Function: updateAccount
 *
 * Modifies attributes of an existing GL account. Structural fields (code,
 * type, classification, currency restriction) become immutable once the
 * account has posted transactions.
 */
export async function run<CF extends Record<string, unknown>>(
  db: Transaction,
  input: UpdateAccountInput & Omit<Partial<CF>, "status">,
  ctx: CommandContext,
  deps?: GetCurrencyDep,
) {
  const {
    id,
    name,
    code,
    accountType,
    classification,
    reconciliation,
    currencyRestriction,
    defaultTaxCode,
    tags,
    accountGroupId,
    ...customFields
  } = input;

  // 1. Find account with forUpdate
  const account = await db
    .selectFrom("Account")
    .selectAll()
    .where("id", "=", id)
    .forUpdate()
    .executeTakeFirst();

  if (!account) {
    return err(new AccountNotFoundError(id));
  }

  // 2. Look up parent CoA
  const coa = await db
    .selectFrom("ChartOfAccounts")
    .selectAll()
    .where("id", "=", account.chartOfAccountsId)
    .executeTakeFirst();

  if (!coa || coa.status === "ARCHIVED") {
    return err(new ChartOfAccountsArchivedError(account.chartOfAccountsId));
  }

  // 3. Validate name if provided
  if (name?.trim() === "") {
    return err(new NameRequiredError(id));
  }

  // 4. Check structural field immutability
  const structuralChanged =
    (code !== undefined && code !== account.code) ||
    (accountType !== undefined && accountType !== account.accountType) ||
    (classification !== undefined && classification !== account.classification) ||
    (currencyRestriction !== undefined && currencyRestriction !== account.currencyRestriction);

  if (structuralChanged && account.hasPostedTransactions) {
    return err(new StructuralFieldImmutableError(id));
  }

  // 5. Validate code if changed
  const effectiveCode = code ?? account.code;
  if (code !== undefined && code !== account.code) {
    if (!/^\d+$/.test(code)) {
      return err(new InvalidAccountCodeError(code));
    }

    const existingAccount = await db
      .selectFrom("Account")
      .selectAll()
      .where("chartOfAccountsId", "=", account.chartOfAccountsId)
      .where("code", "=", code)
      .executeTakeFirst();

    if (existingAccount && existingAccount.id !== id) {
      return err(new DuplicateAccountCodeError(code));
    }
  }

  // 6. Classification compatibility
  const effectiveClassification = classification ?? account.classification;
  const effectiveAccountType = accountType ?? account.accountType;
  if (classification !== undefined || accountType !== undefined) {
    const allowedTypes = CLASSIFICATION_TYPE_MAP[effectiveClassification];
    if (allowedTypes && !allowedTypes.includes(effectiveAccountType)) {
      return err(new IncompatibleClassificationError(effectiveClassification));
    }
  }

  // 7. RETAINED_EARNINGS uniqueness
  if (classification === "RETAINED_EARNINGS" && account.classification !== "RETAINED_EARNINGS") {
    const existingRetained = await db
      .selectFrom("Account")
      .selectAll()
      .where("chartOfAccountsId", "=", account.chartOfAccountsId)
      .where("classification", "=", "RETAINED_EARNINGS")
      .executeTakeFirst();

    if (existingRetained && existingRetained.id !== id) {
      return err(new DuplicateRetainedEarningsError(account.chartOfAccountsId));
    }
  }

  // 8. Reconciliation invariant
  const isReconciliationRequired =
    RECONCILIATION_REQUIRED_CLASSIFICATIONS.includes(effectiveClassification);
  if (isReconciliationRequired) {
    if (reconciliation === false) {
      return err(new ReconciliationRequiredError(effectiveClassification));
    }
  }

  // 9. Currency validation
  if (currencyRestriction !== undefined && currencyRestriction !== null) {
    if (deps) {
      const result = await deps.getCurrency(db, { id: currencyRestriction }, ctx);
      if (!result.value.currency?.isActive) {
        return err(new CurrencyNotFoundError(currencyRestriction));
      }
    }
  }

  // 10. Group validation
  const _effectiveGroupId = accountGroupId !== undefined ? accountGroupId : account.accountGroupId;
  if (accountGroupId !== undefined && accountGroupId !== null) {
    const group = await db
      .selectFrom("AccountGroup")
      .selectAll()
      .where("id", "=", accountGroupId)
      .executeTakeFirst();

    if (group?.chartOfAccountsId !== account.chartOfAccountsId) {
      return err(new AccountGroupNotFoundError(accountGroupId));
    }

    // Check code within range if range defined
    if (group.numberRangeStart != null && group.numberRangeEnd != null) {
      const codeNum = parseInt(effectiveCode, 10);
      if (codeNum < group.numberRangeStart || codeNum > group.numberRangeEnd) {
        return err(new CodeOutOfRangeError(effectiveCode));
      }
    }
  } else if (
    code !== undefined &&
    code !== account.code &&
    account.accountGroupId &&
    accountGroupId === undefined
  ) {
    // Code changed, account has existing group, group assignment not being changed
    const group = await db
      .selectFrom("AccountGroup")
      .selectAll()
      .where("id", "=", account.accountGroupId)
      .executeTakeFirst();

    if (group?.numberRangeStart != null && group.numberRangeEnd != null) {
      const codeNum = parseInt(code, 10);
      if (codeNum < group.numberRangeStart || codeNum > group.numberRangeEnd) {
        return err(new CodeOutOfRangeError(code));
      }
    }
  }

  // 11. Build update data
  const RESERVED_KEYS = new Set([
    "id",
    "chartOfAccountsId",
    "name",
    "code",
    "accountType",
    "classification",
    "reconciliation",
    "currencyRestriction",
    "defaultTaxCode",
    "tags",
    "accountGroupId",
    "successorAccountId",
    "hasPostedTransactions",
    "status",
    "createdAt",
    "updatedAt",
  ]);
  const safeCustomFields: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(customFields)) {
    if (!RESERVED_KEYS.has(key)) {
      safeCustomFields[key] = value;
    }
  }

  const updateData: Record<string, unknown> = {
    ...safeCustomFields,
    updatedAt: new Date(),
  };

  if (name !== undefined) updateData.name = name;
  if (code !== undefined) updateData.code = code;
  if (accountType !== undefined) updateData.accountType = accountType;
  if (classification !== undefined) updateData.classification = classification;
  if (reconciliation !== undefined) updateData.reconciliation = reconciliation;
  // Force reconciliation true if classification requires it
  if (isReconciliationRequired && reconciliation === undefined) {
    updateData.reconciliation = true;
  }
  if (currencyRestriction !== undefined) updateData.currencyRestriction = currencyRestriction;
  if (defaultTaxCode !== undefined) updateData.defaultTaxCode = defaultTaxCode;
  if (tags !== undefined) updateData.tags = tags;
  if (accountGroupId !== undefined) updateData.accountGroupId = accountGroupId;

  const updated = await db
    .updateTable("Account")
    .set(updateData)
    .where("id", "=", id)
    .returningAll()
    .executeTakeFirst();

  return ok({ account: updated! });
}
