import {sql, type Column, type SQL} from 'drizzle-orm'

// opaque keyset cursor over a (timestamp desc, id desc) ordering — the one
// pagination scheme every list endpoint shares. encodes the last row's sort
// key so the next page resumes strictly after it, no offset drift

export type Keyset = {createdAt: Date; id: string}

const encodeCursor = ({createdAt, id}: Keyset): string =>
  Buffer.from(`${createdAt.toISOString()}|${id}`).toString(
    'base64url',
  )

const decodeCursor = (cursor: string): Keyset | null => {
  const [iso, id] = Buffer.from(cursor, 'base64url')
    .toString()
    .split('|')
  const createdAt = iso ? new Date(iso) : null
  if (!createdAt || Number.isNaN(createdAt.getTime()) || !id)
    return null
  return {createdAt, id}
}

// ordering + cursor compare at millisecond precision: a JS Date (and thus the
// encoded cursor) is millisecond-precise while postgres timestamps carry
// microseconds. truncating both sides keeps the (time, id) tiebreak exact, so
// rows sharing a millisecond still page correctly
export const ms = (col: Column): SQL =>
  sql`date_trunc('milliseconds', ${col})`

// the (createdAt desc, id desc) ordering every paged list shares; pass the
// same columns to keysetCursorWhere so order and resume agree
export const keysetOrder = (createdAt: Column, id: Column): SQL[] => [
  sql`${ms(createdAt)} desc`,
  sql`${id} desc`,
]

// the same ordering in memory, for merging keyset pages from several
// sources: js dates are already ms-precise, and canonical uuid text order
// matches postgres uuid order. must stay in step with keysetOrder
export const keysetCompareDesc = (a: Keyset, b: Keyset): number =>
  b.createdAt.getTime() - a.createdAt.getTime() ||
  (a.id < b.id ? 1 : a.id > b.id ? -1 : 0)

// rows strictly after the cursor: older timestamps, then smaller ids within
// the same millisecond
const keysetBefore = (
  createdAt: Column,
  id: Column,
  cursor: Keyset,
): SQL =>
  sql`(${ms(createdAt)} < ${cursor.createdAt} or (${ms(createdAt)} = ${cursor.createdAt} and ${id} < ${cursor.id}))`

// decode + resume-after in one step; absent or invalid cursors are ignored
export const keysetCursorWhere = (
  createdAt: Column,
  id: Column,
  cursor?: string,
): SQL | undefined => {
  const decoded = cursor ? decodeCursor(cursor) : null
  return decoded ? keysetBefore(createdAt, id, decoded) : undefined
}

// split limit+1 fetched rows into a page + the next cursor; null when the
// list is exhausted. the caller fetches `limit + 1` to detect more cheaply
export const toPage = <T>(
  rows: T[],
  limit: number,
  toKeyset: (row: T) => Keyset,
): {page: T[]; nextCursor: string | null} => {
  const hasMore = rows.length > limit
  const page = hasMore ? rows.slice(0, limit) : rows
  const last = page.at(-1)
  return {
    page,
    nextCursor: hasMore && last ? encodeCursor(toKeyset(last)) : null,
  }
}
