import { ValidationError } from '../errors/index.js';

/**
 * Money arithmetic in integer centavos.
 *
 * SPEC.md §35 forbids floating-point money. Every calculation in this module
 * operates on whole centavos; conversion to a decimal string happens only at
 * the persistence boundary (PostgreSQL `NUMERIC(14,2)`) and at display time.
 *
 * The currency is PHP throughout — the institution transacts in pesos only, so
 * a multi-currency abstraction would be unused complexity (ASSUMPTIONS.md E6).
 */

/** A whole number of centavos. 1 peso = 100 centavos. */
export type Centavos = number;

export const ZERO: Centavos = 0;

const MAX_CENTAVOS = 999_999_999_999; // fits NUMERIC(14,2)

function assertInteger(value: number, label: string): void {
  if (!Number.isFinite(value) || !Number.isInteger(value)) {
    throw new ValidationError(`${label} must be a whole number of centavos.`);
  }
  if (Math.abs(value) > MAX_CENTAVOS) {
    throw new ValidationError(`${label} exceeds the maximum representable amount.`);
  }
}

/**
 * Parses a peso amount into centavos.
 *
 * Accepts a number (`1234.5`), or a string that may carry a peso sign, thousand
 * separators or surrounding whitespace (`"₱1,234.50"`). More than two decimal
 * places is rejected rather than silently rounded, because a third decimal in a
 * fee table is a data-entry error, not a rounding question.
 */
export function pesosToCentavos(input: number | string): Centavos {
  if (typeof input === 'number') {
    if (!Number.isFinite(input)) {
      throw new ValidationError('Amount must be a finite number.');
    }
    const scaled = Math.round(input * 100);
    // Guard against binary-float drift for values like 1234.565.
    if (Math.abs(input * 100 - scaled) > 0.5) {
      throw new ValidationError('Amount has more precision than centavos allow.');
    }
    assertInteger(scaled, 'Amount');
    return scaled;
  }

  const cleaned = input.trim().replace(/[₱\s,]/g, '');
  if (cleaned === '') {
    throw new ValidationError('Amount is required.');
  }
  if (!/^-?\d+(\.\d{1,2})?$/.test(cleaned)) {
    throw new ValidationError(
      'Amount must be a peso value with at most two decimal places, for example 12500.00.',
    );
  }

  const negative = cleaned.startsWith('-');
  const [whole = '0', fraction = ''] = cleaned.replace('-', '').split('.');
  const centavos = Number(whole) * 100 + Number(fraction.padEnd(2, '0'));
  assertInteger(centavos, 'Amount');
  return negative ? -centavos : centavos;
}

/** Renders centavos as a plain decimal string, e.g. `12500.00`. Never localized. */
export function centavosToDecimalString(value: Centavos): string {
  assertInteger(value, 'Amount');
  const negative = value < 0;
  const absolute = Math.abs(value);
  const whole = Math.floor(absolute / 100);
  const fraction = absolute % 100;
  return `${negative ? '-' : ''}${whole}.${String(fraction).padStart(2, '0')}`;
}

/** Reads a `NUMERIC(14,2)` value (string from the driver) back into centavos. */
export function decimalStringToCentavos(value: string): Centavos {
  return pesosToCentavos(value);
}

export function add(...values: readonly Centavos[]): Centavos {
  const total = values.reduce((sum, value) => {
    assertInteger(value, 'Amount');
    return sum + value;
  }, 0);
  assertInteger(total, 'Total');
  return total;
}

export function subtract(minuend: Centavos, subtrahend: Centavos): Centavos {
  assertInteger(minuend, 'Amount');
  assertInteger(subtrahend, 'Amount');
  return minuend - subtrahend;
}

/**
 * Multiplies an amount by a whole quantity, e.g. a per-unit fee by unit count.
 * The quantity must be an integer — fractional units are not a thing.
 */
export function multiplyByQuantity(amount: Centavos, quantity: number): Centavos {
  assertInteger(amount, 'Amount');
  if (!Number.isInteger(quantity) || quantity < 0) {
    throw new ValidationError('Quantity must be a non-negative whole number.');
  }
  const result = amount * quantity;
  assertInteger(result, 'Total');
  return result;
}

/**
 * Applies a percentage, rounding half away from zero so that a 50-centavo
 * remainder always resolves in the same direction regardless of sign. Used for
 * percentage-based fees and for discount lines.
 */
export function percentOf(amount: Centavos, percent: number): Centavos {
  assertInteger(amount, 'Amount');
  if (!Number.isFinite(percent)) {
    throw new ValidationError('Percentage must be a finite number.');
  }
  const raw = (amount * percent) / 100;
  const rounded = raw < 0 ? -Math.round(-raw) : Math.round(raw);
  assertInteger(rounded, 'Amount');
  return rounded;
}

export function isZero(value: Centavos): boolean {
  return value === 0;
}

export function isNegative(value: Centavos): boolean {
  return value < 0;
}

export function max(a: Centavos, b: Centavos): Centavos {
  return a > b ? a : b;
}

export function min(a: Centavos, b: Centavos): Centavos {
  return a < b ? a : b;
}

/** Clamps a value to zero, used when a balance must never display negative. */
export function clampToZero(value: Centavos): Centavos {
  return value < 0 ? 0 : value;
}

/**
 * Splits an amount into `parts` instalments whose sum is exactly the original.
 * The remainder centavos are distributed to the earliest instalments, so a
 * ₱10,000.00 tuition in 3 parts becomes 3333.34 / 3333.33 / 3333.33 — never
 * 3333.33 × 3 with a peso quietly lost.
 */
export function splitEvenly(amount: Centavos, parts: number): Centavos[] {
  assertInteger(amount, 'Amount');
  if (!Number.isInteger(parts) || parts < 1) {
    throw new ValidationError('The number of instalments must be at least 1.');
  }
  const base = Math.trunc(amount / parts);
  const remainder = amount - base * parts;
  return Array.from({ length: parts }, (_, index) => (index < Math.abs(remainder) ? base + Math.sign(remainder) : base));
}

/** Philippine peso display formatting, e.g. `₱12,500.00`. */
export function formatPHP(value: Centavos, options: { showSymbol?: boolean } = {}): string {
  assertInteger(value, 'Amount');
  const { showSymbol = true } = options;
  const decimal = centavosToDecimalString(Math.abs(value));
  const [whole = '0', fraction = '00'] = decimal.split('.');
  const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  const sign = value < 0 ? '-' : '';
  return `${sign}${showSymbol ? '₱' : ''}${grouped}.${fraction}`;
}
