/**
 * Tax bifurcation — pure math. No DB, no DI, no NestJS.
 *
 * Stakeholder model (locked 2026-06-12):
 *
 *   • Pricing masters (transport, activity) store inclusive totals;
 *     bifurcation derives base + per-component tax on save.
 *   • Quote / booking line `unit_price` is BASE going forward.
 *     Hotel + Special: sales types base directly.
 *     Transport + Activity: master's bifurcated base is fetched.
 *   • Per-line `tax_snapshot` JSONB captures slab, components, amounts.
 *     Frozen at save — never recomputed if the slab changes later.
 *   • Quote/Booking totals roll up by summing line snapshots — one tax
 *     row displayed (grouped), per-component breakdown kept for invoice.
 *
 * Rounding source = `currencies.rounding` (int, e.g. 1 = whole AED,
 * 5 = nearest INR 5), with `query_quotes.rounding` overriding per-quote.
 * The caller resolves the effective step; this helper just applies it.
 *
 * Math conventions:
 *   • Inclusive → base:  base = total / (1 + total_rate/100)
 *   • Round half-up (accounting standard), step from currency/quote
 *   • Per-component split: each component independently rounded,
 *     then last component absorbs drift so sum(components) === tax_amount
 *     exactly. Prevents the classic "tax line is 0.01 off" bug.
 *   • Slab null OR total_rate ≤ 0 → no tax. base = total, components = [].
 */

export interface TaxSlabSnapshotInput {
  /** Slab id (FK reference; preserved in snapshot for audit / replay). */
  slab_id: string | null;
  /** Slab display name — captured by value so renames don't break history. */
  slab_name: string | null;
  /** Effective tax rate as a percentage (e.g. 18 for GST 18%). Must be
   *  the sum of the components' rates; helper trusts the caller. */
  total_rate: number;
  /** Components in display order. Empty for exempt / null slab.
   *  Rates are percentages (e.g. CGST=9, SGST=9). */
  components: Array<{ name: string; rate: number }>;
}

export interface TaxComponentBreakdown {
  name: string;
  rate: number;
  amount: number;
}

export interface TaxBifurcation {
  base: number;
  tax_amount: number;
  total: number;
  components: TaxComponentBreakdown[];
  /** Slab + component shape frozen for audit / replay. Mirrors the
   *  input slab but with computed amounts attached to components. */
  snapshot: {
    slab_id: string | null;
    slab_name: string | null;
    total_rate: number;
    components: TaxComponentBreakdown[];
  };
}

/**
 * Round a value to the nearest multiple of `step`. step=1 → whole
 * units, step=5 → nearest 5, etc. step≤0 falls back to step=1 (defensive
 * — invalid config should be caught upstream but we don't crash here).
 *
 * Uses "round half away from zero" via `Math.round`, which is the
 * accounting convention used elsewhere in the project (currencies +
 * markup math). Negative inputs are reflected — the project doesn't
 * use negative prices today but the math stays correct if it ever does.
 */
export function roundToStep(value: number, step: number): number {
  const s = step > 0 ? step : 1;
  return Math.round(value / s) * s;
}

/**
 * Bifurcate an inclusive total into base + per-component tax.
 *
 *   bifurcateInclusive(118, { total_rate: 18, components: [
 *     { name: 'CGST', rate: 9 }, { name: 'SGST', rate: 9 },
 *   ] }, 1)
 *   → { base: 100, tax_amount: 18,
 *       components: [
 *         { name: 'CGST', rate: 9, amount: 9 },
 *         { name: 'SGST', rate: 9, amount: 9 },
 *       ],
 *       total: 118 }
 *
 * `slab` null OR total_rate ≤ 0 → exempt (base = total, no tax).
 * `rounding` defaults to 1 (whole units) to match the columns'
 * `default: 1` so callers can omit it for the common case.
 */
export function bifurcateInclusive(
  total: number,
  slab: TaxSlabSnapshotInput | null,
  rounding: number = 1,
): TaxBifurcation {
  if (!isTaxable(slab)) return exemptResult(total, slab);

  const rate = slab!.total_rate;
  // Inclusive total = base × (1 + rate/100). Solve for base.
  const rawBase = total / (1 + rate / 100);
  const base = roundToStep(rawBase, rounding);
  const taxAmount = roundToStep(total - base, rounding);
  const components = splitComponents(taxAmount, slab!, rounding);
  return result(base, taxAmount, components, total, slab!);
}

/**
 * Apply tax on a base amount → returns base, tax, components, total.
 * Symmetric counterpart to `bifurcateInclusive`. Used when sales types
 * a base price (hotel / special lines) and the destination slab
 * computes the tax on top.
 */
export function applyTaxOnBase(
  base: number,
  slab: TaxSlabSnapshotInput | null,
  rounding: number = 1,
): TaxBifurcation {
  if (!isTaxable(slab)) return exemptResult(base, slab);

  const rate = slab!.total_rate;
  const roundedBase = roundToStep(base, rounding);
  const rawTax = roundedBase * (rate / 100);
  const taxAmount = roundToStep(rawTax, rounding);
  const components = splitComponents(taxAmount, slab!, rounding);
  return result(roundedBase, taxAmount, components, roundedBase + taxAmount, slab!);
}

/**
 * Just the base, when callers don't need the full bifurcation
 * (e.g. transport-pricing lookup returns base to the quote line —
 * the snapshot is computed separately at quote save time using the
 * line's effective slab, which may differ from the master's slab).
 */
export function extractBase(
  total: number,
  slab: TaxSlabSnapshotInput | null,
  rounding: number = 1,
): number {
  if (!isTaxable(slab)) return total;
  return roundToStep(total / (1 + slab!.total_rate / 100), rounding);
}

// ─── internals ──────────────────────────────────────────────────────────────

function isTaxable(slab: TaxSlabSnapshotInput | null): boolean {
  if (!slab) return false;
  if (slab.total_rate == null) return false;
  if (slab.total_rate <= 0) return false;
  return true;
}

/**
 * Split a tax amount across components proportional to each component's
 * rate within the slab. Last component absorbs rounding drift so the
 * sum exactly equals the tax amount — accountants verify by adding the
 * column, so off-by-1-paisa breaks reports even though it's "correct".
 */
function splitComponents(
  taxAmount: number,
  slab: TaxSlabSnapshotInput,
  rounding: number,
): TaxComponentBreakdown[] {
  if (slab.components.length === 0) {
    // Slab has a total_rate but no listed components — model as one
    // anonymous component so the row at least appears in the breakdown.
    return [{ name: slab.slab_name ?? 'Tax', rate: slab.total_rate, amount: taxAmount }];
  }
  if (slab.components.length === 1) {
    // Single component → it gets the whole tax amount, no split.
    const c = slab.components[0];
    return [{ name: c.name, rate: c.rate, amount: taxAmount }];
  }

  const totalRate = slab.total_rate || 1;
  const out: TaxComponentBreakdown[] = [];
  let accumulated = 0;
  for (let i = 0; i < slab.components.length - 1; i++) {
    const c = slab.components[i];
    const raw = taxAmount * (c.rate / totalRate);
    const amount = roundToStep(raw, rounding);
    accumulated += amount;
    out.push({ name: c.name, rate: c.rate, amount });
  }
  // Last component absorbs drift.
  const last = slab.components[slab.components.length - 1];
  out.push({ name: last.name, rate: last.rate, amount: +(taxAmount - accumulated).toFixed(2) });
  return out;
}

function exemptResult(amount: number, slab: TaxSlabSnapshotInput | null): TaxBifurcation {
  const snapshot = {
    slab_id: slab?.slab_id ?? null,
    slab_name: slab?.slab_name ?? null,
    total_rate: 0,
    components: [] as TaxComponentBreakdown[],
  };
  return {
    base: amount,
    tax_amount: 0,
    total: amount,
    components: [],
    snapshot,
  };
}

function result(
  base: number,
  taxAmount: number,
  components: TaxComponentBreakdown[],
  total: number,
  slab: TaxSlabSnapshotInput,
): TaxBifurcation {
  return {
    base,
    tax_amount: taxAmount,
    total,
    components,
    snapshot: {
      slab_id: slab.slab_id,
      slab_name: slab.slab_name,
      total_rate: slab.total_rate,
      components,
    },
  };
}
