import type { Transaction } from "../generated/kysely-tailordb";
import {
  NameRequiredError,
  CompanyNotFoundError,
  TemplateNotFoundError,
  TemplateProviderNotConfiguredError,
} from "../lib/errors.generated";
import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/module";

export interface CreateChartOfAccountsInput {
  name: string;
  companyId: string;
  description?: string | null;
  templateId?: string;
}

export interface TemplateAccountGroup {
  code: string;
  name: string;
  parentCode?: string | null;
  sortOrder?: number | null;
  numberRangeStart?: number | null;
  numberRangeEnd?: number | null;
}

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

export interface TemplateSnapshot {
  accountGroups: TemplateAccountGroup[];
  accounts: TemplateAccount[];
}

export interface TemplateProvider {
  resolveTemplate: (id: string) => Promise<TemplateSnapshot | null>;
}

export interface OrganizationQueries {
  getCompany: (
    db: Transaction,
    input: { id: string },
    ctx: CommandContext,
  ) => Promise<{
    ok: true;
    value: { company: { status: string; baseCurrencyId?: string | null } | null };
  }>;
}

/**
 * Function: createChartOfAccounts
 *
 * Establishes a new Chart of Accounts container in DRAFT status for a company.
 * The CoA inherits its base currency from the parent company.
 */
export async function run<CF extends Record<string, unknown>>(
  db: Transaction,
  input: CreateChartOfAccountsInput & CF,
  ctx: CommandContext,
  organizationQueries?: Pick<OrganizationQueries, "getCompany">,
  templateProvider?: TemplateProvider,
) {
  const { name, companyId, description, templateId, ...customFields } = input;

  // 1. Validate company exists and is ACTIVE, and retrieve base currency
  let baseCurrencyId: string;
  if (organizationQueries) {
    const { company } = (await organizationQueries.getCompany(db, { id: companyId }, ctx)).value;
    if (company?.status !== "ACTIVE") {
      return err(new CompanyNotFoundError(companyId));
    }
    // ACTIVE companies are guaranteed to have a base currency assigned
    baseCurrencyId = company.baseCurrencyId ?? "";
  } else {
    // Fallback: check Company stub table directly
    const company = await db
      .selectFrom("Company")
      .selectAll()
      .where("id", "=", companyId)
      .executeTakeFirst();

    if (!company) {
      return err(new CompanyNotFoundError(companyId));
    }
    baseCurrencyId = ((company as Record<string, unknown>).baseCurrencyId as string) ?? "";
  }

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

  // 3. Handle template
  let template: TemplateSnapshot | null = null;
  if (templateId) {
    if (!templateProvider) {
      return err(new TemplateProviderNotConfiguredError(templateId));
    }
    template = await templateProvider.resolveTemplate(templateId);
    if (!template) {
      return err(new TemplateNotFoundError(templateId));
    }
  }

  // 4. Create CoA in DRAFT status with inherited base currency
  const coa = await db
    .insertInto("ChartOfAccounts")
    .values({
      ...(customFields as Record<string, unknown>),
      name,
      companyId,
      baseCurrencyId,
      description: description ?? null,
      status: "DRAFT",
      createdAt: new Date(),
      updatedAt: null,
    })
    .returningAll()
    .executeTakeFirst();

  // 5. If template resolved, copy account groups and accounts
  if (template && coa) {
    const groupCodeToId: Record<string, string> = {};

    // Topological sort: ensure parents are inserted before children
    const sorted = topologicalSortGroups(template.accountGroups);

    // Insert account groups (parents first, then children)
    for (const group of sorted) {
      const parentId = group.parentCode ? (groupCodeToId[group.parentCode] ?? null) : null;
      const insertedGroup = await db
        .insertInto("AccountGroup")
        .values({
          code: group.code,
          name: group.name,
          chartOfAccountsId: coa.id,
          parentAccountGroupId: parentId,
          sortOrder: group.sortOrder ?? null,
          numberRangeStart: group.numberRangeStart ?? null,
          numberRangeEnd: group.numberRangeEnd ?? null,
          createdAt: new Date(),
          updatedAt: null,
        })
        .returningAll()
        .executeTakeFirst();

      if (insertedGroup) {
        groupCodeToId[group.code] = insertedGroup.id;
      }
    }

    // Insert accounts
    for (const account of template.accounts) {
      const groupId = account.groupCode ? (groupCodeToId[account.groupCode] ?? null) : null;
      await db
        .insertInto("Account")
        .values({
          code: account.code,
          name: account.name,
          accountType: account.accountType,
          classification: account.classification,
          chartOfAccountsId: coa.id,
          accountGroupId: groupId,
          currencyRestriction: null,
          reconciliation: account.reconciliation ?? false,
          defaultTaxCode: account.defaultTaxCode ?? null,
          tags: account.tags ?? null,
          successorAccountId: null,
          hasPostedTransactions: false,
          status: "DRAFT",
          createdAt: new Date(),
          updatedAt: null,
        })
        .returningAll()
        .executeTakeFirst();
    }
  }

  return ok({ chartOfAccounts: coa! });
}

function topologicalSortGroups(groups: TemplateAccountGroup[]): TemplateAccountGroup[] {
  const byCode = new Map(groups.map((g) => [g.code, g]));
  const sorted: TemplateAccountGroup[] = [];
  const visited = new Set<string>();

  function visit(group: TemplateAccountGroup) {
    if (visited.has(group.code)) return;
    visited.add(group.code);
    if (group.parentCode) {
      const parent = byCode.get(group.parentCode);
      if (parent) visit(parent);
    }
    sorted.push(group);
  }

  for (const group of groups) {
    visit(group);
  }

  return sorted;
}
