/**
 * Profit & Loss (Income Statement) engine.
 *
 * Implements the P&L calculation specification:
 *  - A flow report over an inclusive [period_start, period_end] date range (§2).
 *  - Only temporary/nominal accounts (REVENUE / EXPENSE) — never balance-sheet
 *    accounts (§2, §7.1).
 *  - Per-account normalization to a positive figure on its natural side (§3),
 *    with contra-revenue / contra-expense SUBTRACTED (§3).
 *  - The multi-step subtotal chain, each derived from the line above it (§4).
 *  - Optional margins as presentation-only ratios (§5).
 *  - Reconciliation against the balance-sheet retained-earnings movement (§6).
 *  - The validation checks of §7.
 *
 * Numeric rule (§9): ALL monetary amounts are integer **minor units** (cents).
 * No binary floats are used in any sum or in the subtotal chain. Only margin
 * ratios (§5) are fractional, and they are presentation-only — never fed back
 * into a currency total.
 */

// ── Types ─────────────────────────────────────────────────────────────────────

export type NormalBalance = "DEBIT" | "CREDIT";

/** Explicit per-account sub-classification (§4 — never inferred from names). */
export type PnlClassification =
  | "REVENUE"
  | "CONTRA_REVENUE"
  | "COGS"
  | "CONTRA_COGS"
  | "OPERATING_EXPENSE"
  | "OTHER_INCOME"
  | "OTHER_EXPENSE"
  | "TAX_EXPENSE";

/** Top-level account type, used only for the type-purity check (§7.1). */
export type AccountType = "asset" | "liability" | "equity" | "income" | "expense";

export type PnlAccount = {
  id: string;
  name: string;
  /** Top-level type. Only `income` / `expense` are nominal and may appear. */
  type: AccountType;
  /** Explicit sub-classification that drives the multi-step structure. */
  classification: PnlClassification;
};

/** A ledger posting in integer minor units. `date` is the recognition date
 *  for the chosen basis (§8). */
export type Posting = {
  accountId: string;
  debitMinor: number;
  creditMinor: number;
  date: string; // YYYY-MM-DD
};

export type Basis = "accrual" | "cash";

export type PnlOptions = {
  periodStart: string; // YYYY-MM-DD, inclusive
  periodEnd: string;   // YYYY-MM-DD, inclusive
  /** Documentation/label only — the caller supplies postings already filtered
   *  to the recognition events for this basis (§8). Default: accrual. */
  basis?: Basis;
};

export type Severity = "error" | "warning";
export type ValidationIssue = { code: string; severity: Severity; message: string; accountId?: string };

export type PnlLine = {
  accountId: string;
  name: string;
  classification: PnlClassification;
  /** Normalized, positive-on-natural-side amount in minor units (§3). */
  amountMinor: number;
};

export type PnlSubtotals = {
  grossRevenue: number;
  contraRevenue: number;
  netRevenue: number;
  costOfGoodsSold: number;
  grossProfit: number;
  operatingExpenses: number;
  operatingIncome: number; // EBIT
  otherIncome: number;
  otherExpense: number;
  otherNet: number;
  preTaxIncome: number; // EBT
  incomeTaxExpense: number;
  netIncome: number;
};

export type PnlResult = {
  periodStart: string;
  periodEnd: string;
  basis: Basis;
  lines: PnlLine[];
  subtotals: PnlSubtotals;
  /** Single-step view: totals that satisfy `netIncome = totalRevenue - totalExpenses`. */
  totalRevenue: number;
  totalExpenses: number;
  issues: ValidationIssue[];
};

export type Margins = {
  grossMargin: number | null;
  operatingMargin: number | null;
  netMargin: number | null;
};

// ── Money helpers ─────────────────────────────────────────────────────────────

/** Convert a decimal currency value to integer minor units (cents). */
export function toMinor(amount: number | string): number {
  const n = typeof amount === "string" ? Number(amount) : amount;
  // Round half-away-from-zero at the cent to avoid float drift before integers.
  return Math.sign(n) * Math.round(Math.abs(n) * 100);
}

/** Convert integer minor units back to a decimal currency value (presentation). */
export function fromMinor(minor: number): number {
  return minor / 100;
}

// ── Classification metadata ─────────────────────────────────────────────────

/** The natural (normal) balance side of each classification (§3). */
export function normalBalanceOf(c: PnlClassification): NormalBalance {
  switch (c) {
    case "REVENUE":
    case "CONTRA_COGS":
    case "OTHER_INCOME":
      return "CREDIT";
    case "CONTRA_REVENUE":
    case "COGS":
    case "OPERATING_EXPENSE":
    case "OTHER_EXPENSE":
    case "TAX_EXPENSE":
      return "DEBIT";
  }
}

const CONTRA: ReadonlySet<PnlClassification> = new Set(["CONTRA_REVENUE", "CONTRA_COGS"]);

// ── Core computation ──────────────────────────────────────────────────────────

/**
 * Compute a Profit & Loss for the given postings and account metadata.
 * Throws on hard validation errors (type purity, broken subtotal chain).
 */
export function computePnL(
  postings: Posting[],
  accounts: PnlAccount[],
  options: PnlOptions,
): PnlResult {
  const { periodStart, periodEnd } = options;
  const basis = options.basis ?? "accrual";
  if (!periodStart || !periodEnd) throw new Error("computePnL: periodStart and periodEnd are required");
  if (periodStart > periodEnd) throw new Error("computePnL: periodStart must be <= periodEnd");

  const acctById = new Map(accounts.map((a) => [a.id, a]));
  const issues: ValidationIssue[] = [];

  // 1) Filter to the inclusive window (§2). YYYY-MM-DD compares lexicographically.
  const inWindow = postings.filter((p) => p.date >= periodStart && p.date <= periodEnd);

  // 2) Accumulate raw activity (debit - credit) per account, in minor units (§3).
  const rawByAccount = new Map<string, number>();
  for (const p of inWindow) {
    const acct = acctById.get(p.accountId);
    if (!acct) {
      issues.push({ code: "UNKNOWN_ACCOUNT", severity: "error", accountId: p.accountId,
        message: `Posting references unknown account ${p.accountId}` });
      continue;
    }
    // Type purity (§7.1): only nominal (income/expense) accounts may appear.
    if (acct.type !== "income" && acct.type !== "expense") {
      issues.push({ code: "TYPE_PURITY", severity: "error", accountId: acct.id,
        message: `Account "${acct.name}" has balance-sheet type "${acct.type}" and must not appear on the P&L` });
    }
    const prev = rawByAccount.get(p.accountId) ?? 0;
    rawByAccount.set(p.accountId, prev + (p.debitMinor | 0) - (p.creditMinor | 0));
  }

  // 3) Normalize each account to a positive-on-natural-side amount (§3).
  const lines: PnlLine[] = [];
  for (const [accountId, raw] of rawByAccount) {
    const acct = acctById.get(accountId)!;
    const nb = normalBalanceOf(acct.classification);
    const amountMinor = nb === "CREDIT" ? -raw : raw;
    lines.push({ accountId, name: acct.name, classification: acct.classification, amountMinor });

    // Contra direction (§7.3): a contra line must reduce its parent, i.e. be >= 0
    // after normalization. A net-negative contra would *increase* the parent.
    if (CONTRA.has(acct.classification) && amountMinor < 0) {
      issues.push({ code: "CONTRA_DIRECTION", severity: "error", accountId,
        message: `Contra account "${acct.name}" nets to ${amountMinor} minor units and would increase its parent line` });
    }
  }
  lines.sort((a, b) => a.classification.localeCompare(b.classification) || a.name.localeCompare(b.name));

  // Sum helper over a classification.
  const sumOf = (c: PnlClassification) =>
    lines.reduce((s, l) => (l.classification === c ? s + l.amountMinor : s), 0);

  // 4) Multi-step subtotal chain — each derived from the line above (§4).
  const grossRevenue = sumOf("REVENUE");
  const contraRevenue = sumOf("CONTRA_REVENUE");
  const netRevenue = grossRevenue - contraRevenue;

  const costOfGoodsSold = sumOf("COGS") - sumOf("CONTRA_COGS");
  const grossProfit = netRevenue - costOfGoodsSold;

  const operatingExpenses = sumOf("OPERATING_EXPENSE");
  const operatingIncome = grossProfit - operatingExpenses;

  const otherIncome = sumOf("OTHER_INCOME");
  const otherExpense = sumOf("OTHER_EXPENSE");
  const otherNet = otherIncome - otherExpense;
  const preTaxIncome = operatingIncome + otherNet;

  const incomeTaxExpense = sumOf("TAX_EXPENSE");
  const netIncome = preTaxIncome - incomeTaxExpense;

  // Single-step totals (§4 single-step layout). Defined so that
  // netIncome === totalRevenue - totalExpenses.
  const totalRevenue = netRevenue + otherIncome;
  const totalExpenses = costOfGoodsSold + operatingExpenses + otherExpense + incomeTaxExpense;

  // 7.2) Subtotal-chain check: recompute Net Income both ways and assert equality.
  const direct = totalRevenue - totalExpenses;
  if (direct !== netIncome) {
    throw new Error(`P&L subtotal chain broken: chain net income ${netIncome} != direct ${direct} (minor units)`);
  }

  // 7.5) Sign sanity: a negative net revenue is only possible with returns
  // exceeding sales — warn, do not error.
  if (netRevenue < 0) {
    issues.push({ code: "NEGATIVE_NET_REVENUE", severity: "warning",
      message: `Net revenue is negative (${netRevenue} minor units) — contra-revenue exceeds gross revenue` });
  }

  // Hard errors abort (§7.1 "fail loudly").
  const errors = issues.filter((i) => i.severity === "error");
  if (errors.length > 0) {
    throw new PnlValidationError(errors[0].message, issues);
  }

  return {
    periodStart, periodEnd, basis,
    lines,
    subtotals: {
      grossRevenue, contraRevenue, netRevenue,
      costOfGoodsSold, grossProfit,
      operatingExpenses, operatingIncome,
      otherIncome, otherExpense, otherNet,
      preTaxIncome, incomeTaxExpense, netIncome,
    },
    totalRevenue, totalExpenses,
    issues,
  };
}

export class PnlValidationError extends Error {
  issues: ValidationIssue[];
  constructor(message: string, issues: ValidationIssue[]) {
    super(message);
    this.name = "PnlValidationError";
    this.issues = issues;
  }
}

// ── Margins (§5) ──────────────────────────────────────────────────────────────

/** Presentation-only ratios. Returns null for any ratio when net revenue is 0. */
export function computeMargins(r: PnlResult): Margins {
  const nr = r.subtotals.netRevenue;
  if (nr === 0) return { grossMargin: null, operatingMargin: null, netMargin: null };
  return {
    grossMargin: r.subtotals.grossProfit / nr,
    operatingMargin: r.subtotals.operatingIncome / nr,
    netMargin: r.subtotals.netIncome / nr,
  };
}

// ── Reconciliation with the balance sheet (§6) ────────────────────────────────

export type Reconciliation = {
  ok: boolean;
  netIncomeMinor: number;
  expectedMinor: number;       // RE(end) - RE(start) + dividends
  differenceMinor: number;     // netIncome - expected
};

/**
 * Net Income MUST equal the period's contribution to Retained Earnings:
 *   netIncome == RE(period_end) - RE(period_start) + dividends declared in period
 * All inputs are integer minor units.
 */
export function reconcileWithRetainedEarnings(
  netIncomeMinor: number,
  retainedEarningsStartMinor: number,
  retainedEarningsEndMinor: number,
  dividendsDeclaredMinor = 0,
): Reconciliation {
  const expectedMinor = retainedEarningsEndMinor - retainedEarningsStartMinor + dividendsDeclaredMinor;
  const differenceMinor = netIncomeMinor - expectedMinor;
  return { ok: differenceMinor === 0, netIncomeMinor, expectedMinor, differenceMinor };
}
