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

export interface CreateAccountInput {
  chartOfAccountsId: string;
  code: string;
  name: string;
  accountType: "ASSET" | "LIABILITY" | "EQUITY" | "REVENUE" | "EXPENSE";
  classification?:
    | "REGULAR"
    | "RECEIVABLE"
    | "PAYABLE"
    | "BANK_CASH"
    | "FIXED_ASSET"
    | "RETAINED_EARNINGS"
    | "NON_POSTING";
  reconciliation?: boolean;
  currencyRestriction?: string;
  defaultTaxCode?: string;
  tags?: string;
  accountGroupId?: string;
}

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: createAccount
 *
 * Establishes a new GL account in DRAFT status within a Chart of Accounts.
 * Each account carries a structured numeric code, name, account type,
 * classification, and optional attributes.
 */
export async function run<CF extends Record<string, unknown>>(
  db: Transaction,
  input: CreateAccountInput & CF,
  ctx: CommandContext,
  deps?: GetCurrencyDep,
) {
  const {
    chartOfAccountsId,
    code,
    name,
    accountType,
    classification: classificationInput,
    reconciliation: reconciliationInput,
    currencyRestriction,
    defaultTaxCode,
    tags,
    accountGroupId,
    ...customFields
  } = input;

  const classification = classificationInput ?? "REGULAR";

  // 1. Validate name non-empty
  if (!name || name.trim() === "") {
    return err(new NameRequiredError(name ?? ""));
  }

  // 2. Validate account code is numeric
  if (!/^\d+$/.test(code)) {
    return err(new InvalidAccountCodeError(code));
  }

  // 3. Look up CoA - must exist and be DRAFT or ACTIVE
  const coa = await db
    .selectFrom("ChartOfAccounts")
    .selectAll()
    .where("id", "=", chartOfAccountsId)
    .executeTakeFirst();

  if (!coa) {
    return err(new ChartOfAccountsNotFoundError(chartOfAccountsId));
  }

  if (coa.status === "ARCHIVED") {
    return err(new ChartOfAccountsNotFoundError(chartOfAccountsId));
  }

  // 4. Check account code uniqueness within CoA
  const existingAccount = await db
    .selectFrom("Account")
    .selectAll()
    .where("chartOfAccountsId", "=", chartOfAccountsId)
    .where("code", "=", code)
    .forUpdate()
    .executeTakeFirst();

  if (existingAccount) {
    return err(new DuplicateAccountCodeError(code));
  }

  // 5. Classification compatibility check
  const allowedTypes = CLASSIFICATION_TYPE_MAP[classification];
  if (allowedTypes && !allowedTypes.includes(accountType)) {
    return err(new IncompatibleClassificationError(classification));
  }

  // 6. RETAINED_EARNINGS uniqueness per CoA
  if (classification === "RETAINED_EARNINGS") {
    const existingRetained = await db
      .selectFrom("Account")
      .selectAll()
      .where("chartOfAccountsId", "=", chartOfAccountsId)
      .where("classification", "=", "RETAINED_EARNINGS")
      .executeTakeFirst();

    if (existingRetained) {
      return err(new DuplicateRetainedEarningsError(chartOfAccountsId));
    }
  }

  // 7. Currency validation (cross-module)
  if (currencyRestriction) {
    if (deps) {
      const result = await deps.getCurrency(db, { id: currencyRestriction }, ctx);
      if (!result.value.currency?.isActive) {
        return err(new CurrencyNotFoundError(currencyRestriction));
      }
    }
  }

  // 8. Group validation
  if (accountGroupId) {
    const group = await db
      .selectFrom("AccountGroup")
      .selectAll()
      .where("id", "=", accountGroupId)
      .executeTakeFirst();

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

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

  // 9. Reconciliation validation
  const isReconciliationRequired = RECONCILIATION_REQUIRED_CLASSIFICATIONS.includes(classification);
  if (isReconciliationRequired && reconciliationInput === false) {
    return err(new ReconciliationRequiredError(classification));
  }
  const reconciliation = isReconciliationRequired ? true : (reconciliationInput ?? false);

  // 10. Insert account in DRAFT status
  const account = await db
    .insertInto("Account")
    .values({
      ...(customFields as Record<string, unknown>),
      chartOfAccountsId,
      code,
      name,
      accountType,
      classification,
      reconciliation,
      currencyRestriction: currencyRestriction ?? null,
      defaultTaxCode: defaultTaxCode ?? null,
      tags: tags ?? null,
      accountGroupId: accountGroupId ?? null,
      successorAccountId: null,
      hasPostedTransactions: false,
      status: "DRAFT",
      createdAt: new Date(),
      updatedAt: null,
    })
    .returningAll()
    .executeTakeFirst();

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