import {type Snapshot} from '@/lib/snapshot-cache'

// bounded, touch-ordered persistence for keyed caches (detail-by-id and
// paged-by-id). the layer-1 snapshots bound a single value; a keyed cache maps
// many ids to values and would grow the disk without limit as the user browses,
// so only the n most-recently-written ids survive. persisted as an mru-ordered
// [id, value] list so touch order outlives a cold start, wrapping one snapshot
// so it inherits the envelope versioning and identical-write dedupe

export type KeyedSnapshot<T> = {
  seed: () => Record<string, T>
  // persist the ids whose value changed between prev and next, capped to the
  // most-recently-touched max
  sync: (next: Record<string, T>, prev: Record<string, T>) => void
  clear: () => void
}

type KeyedConfig<T, P> = {
  max: number
  // data-only projection of the in-memory value (never loading/error flags);
  // undefined skips the id, eg a slot still loading with no data yet
  project: (value: T) => P | undefined
  revive: (persisted: P) => T
}

export const createKeyedSnapshot = <T, P>(
  entries: Snapshot<[string, P][]>,
  {max, project, revive}: KeyedConfig<T, P>,
): KeyedSnapshot<T> => {
  // mru order, front = most recent; seeded from disk, maintained on every sync
  let order: string[] = []
  return {
    seed: () => {
      const persisted = entries.read() ?? []
      order = persisted.map(([id]) => id)
      return Object.fromEntries(
        persisted.map(([id, value]) => [id, revive(value)]),
      )
    },
    sync: (next, prev) => {
      const changed = new Set<string>()
      for (const id in next)
        if (next[id] !== prev[id] && project(next[id]!) !== undefined)
          changed.add(id)
      // a persisted id deleted from the map must also rewrite, or the
      // removed entity would reseed byId on the next cold start
      const removed = order.some((id) => !(id in next))
      if (!changed.size && !removed) return
      // touched ids to the front, then the surviving tail, then evict the cold
      order = [
        ...changed,
        ...order.filter((id) => !changed.has(id) && id in next),
      ].slice(0, max)
      const kept: [string, P][] = []
      for (const id of order) {
        const value = project(next[id]!)
        if (value !== undefined) kept.push([id, value])
      }
      entries.write(kept)
    },
    clear: () => {
      order = []
      entries.clear()
    },
  }
}
