import { clampLimit, decodeCursor, encodeCursor } from './cursor.js'
import { deriveStored } from './derive.js'
import type { Keyset, ListFilter, Page, RecordStore, Stored } from './types.js'

type InMemoryRow<T> = {
  key: Keyset
  stored: Stored<T>
}

export type InMemoryRecordStoreOptions = {
  now?: () => string
  makeId?: () => string
}

function snapshotRecord<T>(record: T): T {
  return JSON.parse(JSON.stringify(record)) as T
}

function snapshotStored<T>(stored: Stored<T>): Stored<T> {
  return {
    id: stored.id,
    createdAt: stored.createdAt,
    record: snapshotRecord(stored.record),
  }
}

function compareKeysetDesc(a: Keyset, b: Keyset): number {
  if (a.createdAtMs !== b.createdAtMs) {
    return b.createdAtMs - a.createdAtMs
  }
  return b.seq - a.seq
}

function isBeforeCursor(key: Keyset, cursor: Keyset): boolean {
  return (
    key.createdAtMs < cursor.createdAtMs ||
    (key.createdAtMs === cursor.createdAtMs && key.seq < cursor.seq)
  )
}

export function makeInMemoryRecordStore<T>(
  opts: InMemoryRecordStoreOptions = {},
): RecordStore<T> {
  const now = opts.now ?? (() => new Date().toISOString())
  const makeId = opts.makeId ?? (() => crypto.randomUUID())

  const rows: InMemoryRow<T>[] = []
  let seqCounter = 0

  return {
    async append(record: T): Promise<Stored<T>> {
      const derived = deriveStored(record, now, makeId)
      const seq = ++seqCounter
      const key: Keyset = { createdAtMs: derived.createdAtMs, seq }
      const stored: Stored<T> = {
        id: derived.id,
        createdAt: derived.createdAt,
        record: derived.record,
      }

      rows.push({ key, stored })
      return snapshotStored(stored)
    },

    async list(filter: ListFilter = {}): Promise<Page<Stored<T>>> {
      let candidates = [...rows].sort((a, b) => compareKeysetDesc(a.key, b.key))

      if (filter.cursor !== undefined) {
        const cursor = decodeCursor(filter.cursor)
        candidates = candidates.filter((row) => isBeforeCursor(row.key, cursor))
      }

      const limit = clampLimit(filter.limit)
      const pageRows = candidates.slice(0, limit)
      const hasMore = candidates.length > limit

      return {
        items: pageRows.map((row) => snapshotStored(row.stored)),
        nextCursor: hasMore
          ? encodeCursor(pageRows[pageRows.length - 1]!.key)
          : null,
      }
    },
  }
}
