import {
  MeteringStorageError,
  MeteringValidationError,
  isMeteringError,
} from './errors.js'
import { isWithinMagnitudeBound, MAX_BIGINT_DIGITS } from './internal/bigint.js'
import type { EntitlementPolicy, MeterKey, PolicyStore, SubjectRef } from './types.js'

export type ResolveDeps = {
  policies: PolicyStore
}

function isValidDate(value: unknown): value is Date {
  return value instanceof Date && Number.isFinite(value.getTime())
}

function assertPolicy(policy: EntitlementPolicy, meter: MeterKey): void {
  if (typeof policy.meter !== 'string' || policy.meter.trim().length === 0) {
    throw new MeteringValidationError('policy.meter', 'must be non-empty')
  }
  if (policy.meter !== meter) {
    throw new MeteringValidationError('policy.meter', 'must match meter')
  }
  if (typeof policy.includedUnits !== 'bigint' || policy.includedUnits < 0n) {
    throw new MeteringValidationError('policy.includedUnits', 'must be a non-negative bigint')
  }
  if (!isWithinMagnitudeBound(policy.includedUnits)) {
    throw new MeteringValidationError(
      'policy.includedUnits',
      `must be below 10^${MAX_BIGINT_DIGITS}`,
    )
  }
  if (policy.hardLimit !== undefined) {
    if (typeof policy.hardLimit !== 'bigint' || policy.hardLimit < policy.includedUnits) {
      throw new MeteringValidationError('policy.hardLimit', 'must be at least includedUnits')
    }
    if (!isWithinMagnitudeBound(policy.hardLimit)) {
      throw new MeteringValidationError('policy.hardLimit', `must be below 10^${MAX_BIGINT_DIGITS}`)
    }
  }
  if (policy.overage !== 'deny' && policy.overage !== 'allow') {
    throw new MeteringValidationError('policy.overage', 'must be deny or allow')
  }
  if (typeof policy.period !== 'object' || policy.period === null) {
    throw new MeteringValidationError('policy.period', 'must be a valid non-empty half-open interval')
  }
  if (
    typeof policy.period.id !== 'string' ||
    policy.period.id.trim().length === 0 ||
    !isValidDate(policy.period.startsAt) ||
    !isValidDate(policy.period.endsAt) ||
    policy.period.startsAt >= policy.period.endsAt
  ) {
    throw new MeteringValidationError('policy.period', 'must be a valid non-empty half-open interval')
  }
  if (typeof policy.version !== 'string' || policy.version.trim().length === 0) {
    throw new MeteringValidationError('policy.version', 'must be non-empty')
  }
}

export async function resolve(
  deps: ResolveDeps,
  subject: SubjectRef,
  meter: MeterKey,
  at: Date,
): Promise<EntitlementPolicy | null> {
  if (typeof meter !== 'string' || meter.trim().length === 0) {
    throw new MeteringValidationError('meter', 'must be non-empty')
  }
  if (!isValidDate(at)) {
    throw new MeteringValidationError('at', 'must be a valid Date')
  }

  let policy: EntitlementPolicy | null
  try {
    policy = await deps.policies.resolve(subject, meter, at)
  } catch (error) {
    if (isMeteringError(error)) throw error
    throw new MeteringStorageError('resolve', error)
  }

  if (policy === null) return null

  if (typeof policy !== 'object' || policy === null) {
    throw new MeteringValidationError('policy', 'must be an object or null')
  }
  assertPolicy(policy, meter)
  return policy
}
