/** Declared unit for a schedule entry — never magnitude-inferred. */
export type RateUnit = 'fraction' | 'percent' | 'basisPoints'

export interface VatScheduleEntry {
  /** Inclusive lower bound, `YYYY-MM-DD`. */
  effectiveFrom: string
  value: string | number
  unit: RateUnit
}

declare const vatRateBrand: unique symbol

/** Opaque basis-points rate — construct only via `resolveVatRate` or the `from*` helpers. */
export type VatRate = bigint & { readonly [vatRateBrand]: true }

const TEN_THOUSAND = 10_000n

/** A rate value failed the basis-points trust-boundary contract (integer, ≥ 0). */
export class InvalidVatRateError extends Error {
  readonly value: string

  constructor(message: string, value: unknown) {
    super(message)
    this.name = 'InvalidVatRateError'
    this.value = String(value)
  }
}

/**
 * Coerce to an exact non-negative integer bigint, or throw `InvalidVatRateError`.
 * The single chokepoint every rate funnels through (`from*` + `resolveVatRate`),
 * so a negative or fractional rate can never reach the BigInt money path —
 * negative inputs fall outside the spec's non-negative domain (trunc≠floor
 * mis-rounding; a negative bp also breaks `extractVat`'s `10000 + bp` denominator).
 */
function toBasisPointsBigInt(bp: bigint | number): bigint {
  let n: bigint
  if (typeof bp === 'number') {
    if (!Number.isInteger(bp)) {
      throw new InvalidVatRateError(`VAT basis points must be an integer, got ${bp}`, bp)
    }
    n = BigInt(bp)
  } else {
    n = bp
  }
  if (n < 0n) {
    throw new InvalidVatRateError(`VAT rate must be non-negative, got ${n} bp`, n)
  }
  return n
}

export function fromBasisPoints(bp: bigint | number): VatRate {
  return toBasisPointsBigInt(bp) as VatRate
}

export function fromPercent(pct: bigint | number): VatRate {
  if (typeof pct === 'number' && !Number.isInteger(pct)) {
    throw new InvalidVatRateError(`VAT percent must be an integer, got ${pct}`, pct)
  }
  return fromBasisPoints(BigInt(pct) * 100n)
}

/** `frac` is a decimal fraction string or number (e.g. `0.1700` → 1700 bp). */
export function fromFraction(frac: string | number): VatRate {
  return fromBasisPoints(fractionToBasisPoints(frac))
}

/**
 * A `date` argument failed the `YYYY-MM-DD` trust-boundary contract.
 * Validated by shape only (no `Date` construction — that would reintroduce the
 * UTC-guess timezone pitfall the string contract exists to remove).
 */
export class InvalidVatDateError extends Error {
  readonly value: string

  constructor(message: string, value: unknown) {
    super(message)
    this.name = 'InvalidVatDateError'
    this.value = String(value)
  }
}

export class NoRateForDateError extends Error {
  readonly date: string
  readonly firstEffectiveFrom: string | undefined

  constructor(date: string, firstEffectiveFrom?: string) {
    super(
      firstEffectiveFrom
        ? `No VAT rate for date ${date} (before first entry ${firstEffectiveFrom})`
        : `No VAT rate for date ${date}`,
    )
    this.name = 'NoRateForDateError'
    this.date = date
    this.firstEffectiveFrom = firstEffectiveFrom
  }
}

export function fractionToBasisPoints(frac: string | number): bigint {
  const s = String(frac)
  if (!/^-?\d*(\.\d+)?$/.test(s) || s === '' || s === '-' || s === '.') {
    throw new InvalidVatRateError(`VAT fraction is not a decimal number: ${s}`, frac)
  }
  const negative = s.startsWith('-')
  const unsigned = negative ? s.slice(1) : s
  const [whole, fracPart = ''] = unsigned.split('.')
  if (/[1-9]/.test(fracPart.slice(4))) {
    throw new InvalidVatRateError(
      `VAT fraction has sub-basis-point precision (max 4 decimals): ${s}`,
      frac,
    )
  }
  const padded = (fracPart + '0000').slice(0, 4)
  const bp = BigInt(whole || '0') * TEN_THOUSAND + BigInt(padded || '0')
  return negative ? -bp : bp
}

export function normalizeToBasisPoints(value: string | number, unit: RateUnit): bigint {
  switch (unit) {
    case 'basisPoints': {
      if (typeof value === 'number') return toBasisPointsBigInt(value)
      if (!/^-?\d+$/.test(value.trim())) {
        throw new InvalidVatRateError(`VAT basis points must be an integer string, got ${value}`, value)
      }
      return toBasisPointsBigInt(BigInt(value.trim()))
    }
    case 'percent': {
      const pct = typeof value === 'number' ? value : Number(value)
      if (!Number.isInteger(pct)) {
        throw new InvalidVatRateError(`VAT percent must be an integer, got ${value}`, value)
      }
      return toBasisPointsBigInt(pct) * 100n
    }
    case 'fraction':
      return toBasisPointsBigInt(fractionToBasisPoints(value))
  }
}

/** A well-formed `YYYY-MM-DD` calendar date (shape-checked, not TZ-resolved). */
const YYYY_MM_DD = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/

/**
 * Last schedule entry with `effectiveFrom ≤ date` (most-recent-on-or-before).
 *
 * `date` is a **host-resolved `YYYY-MM-DD` legal date string** — never a JS
 * `Date`. A `Date` is an instant that only becomes a calendar day *through* a
 * timezone, and this module is country-neutral so it cannot know the legal
 * timezone; the host owns that resolution (see spec Money-math § date-contract).
 * Malformed input throws `InvalidVatDateError` (trust boundary). Throws
 * `NoRateForDateError` when the date precedes every entry.
 */
export function resolveVatRate(
  schedule: readonly VatScheduleEntry[],
  date: string,
): VatRate {
  if (!YYYY_MM_DD.test(date)) {
    throw new InvalidVatDateError(
      `VAT date must be a host-resolved YYYY-MM-DD string, got ${JSON.stringify(date)}`,
      date,
    )
  }

  const sorted = [...schedule].sort((a, b) => a.effectiveFrom.localeCompare(b.effectiveFrom))

  let match: VatScheduleEntry | undefined
  for (const entry of sorted) {
    if (entry.effectiveFrom <= date) {
      match = entry
    } else {
      break
    }
  }

  if (!match) {
    throw new NoRateForDateError(date, sorted[0]?.effectiveFrom)
  }

  return fromBasisPoints(normalizeToBasisPoints(match.value, match.unit))
}
