import { InvalidCursorError } from './errors.js'
import type { Keyset } from './types.js'

const DEFAULT_LIMIT = 50
const MAX_LIMIT = 200

const PLUS = /\+/g
const SLASH = /\//g
const TRAILING_EQ = /=+$/
const DASH = /-/g
const UNDERSCORE = /_/g

function toBase64Url(value: string): string {
  return btoa(value).replace(PLUS, '-').replace(SLASH, '_').replace(TRAILING_EQ, '')
}

function fromBase64Url(value: string): string {
  let base64 = value.replace(DASH, '+').replace(UNDERSCORE, '/')
  const pad = base64.length % 4
  if (pad) {
    base64 += '='.repeat(4 - pad)
  }
  return atob(base64)
}

type CursorPayload = {
  c: number
  s: number
}

function isKeyset(value: unknown): value is CursorPayload {
  return (
    typeof value === 'object' &&
    value !== null &&
    'c' in value &&
    's' in value &&
    typeof value.c === 'number' &&
    typeof value.s === 'number' &&
    Number.isFinite(value.c) &&
    Number.isFinite(value.s)
  )
}

export function encodeCursor(key: Keyset): string {
  const payload: CursorPayload = {
    c: key.createdAtMs,
    s: key.seq,
  }
  return toBase64Url(JSON.stringify(payload))
}

export function decodeCursor(cursor: string): Keyset {
  try {
    const json = fromBase64Url(cursor)
    const parsed: unknown = JSON.parse(json)
    if (!isKeyset(parsed)) {
      throw new InvalidCursorError()
    }
    return { createdAtMs: parsed.c, seq: parsed.s }
  } catch (error) {
    if (error instanceof InvalidCursorError) {
      throw error
    }
    throw new InvalidCursorError()
  }
}

export type ClampLimitOptions = {
  defaultLimit?: number
  maxLimit?: number
}

/** Clamp untrusted `limit`: non-finite/undefined/<=0 → default; huge → max. Never throws. */
export function clampLimit(
  value: number | undefined,
  opts?: ClampLimitOptions,
): number {
  const defaultLimit = opts?.defaultLimit ?? DEFAULT_LIMIT
  const maxLimit = opts?.maxLimit ?? MAX_LIMIT

  if (value === undefined || !Number.isFinite(value) || value <= 0) {
    return defaultLimit
  }

  const truncated = Math.trunc(value)
  if (truncated <= 0) {
    return defaultLimit
  }

  return Math.min(truncated, maxLimit)
}
