/**
 * Keyset cursor over NUMERIC (createdAtMs, seq) — collation-safe across pg dialects.
 * Ported from @platform-modules/comments/src/cursor.ts (spec §5 inbox parity).
 */
export class InvalidCursorError extends Error {
  override readonly name = 'InvalidCursorError'
  constructor() {
    super('invalid pagination cursor')
  }
}

const DEFAULT_LIMIT = 20
const MAX_LIMIT = 200

// Base64url encode/decode (web-standard btoa/atob — no Node dep)
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)
}

export type Keyset = { createdAtMs: number; seq: number }

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 as CursorPayload).c === 'number' &&
    typeof (value as CursorPayload).s === 'number' &&
    Number.isFinite((value as CursorPayload).c) &&
    Number.isFinite((value as CursorPayload).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 (e) {
    if (e instanceof InvalidCursorError) throw e
    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 dflt = opts?.defaultLimit ?? DEFAULT_LIMIT
  const max = opts?.maxLimit ?? MAX_LIMIT
  if (value === undefined || !Number.isFinite(value) || value <= 0) return dflt
  const t = Math.trunc(value)
  if (t <= 0) return dflt
  return Math.min(t, max)
}
