import {create} from 'zustand'

import {type KeyedSnapshot} from '@/lib/keyed-snapshot'
import {
  canLoadMore,
  emptyPaged,
  stepPaged,
  type PagedSlice,
  type PageFetcher,
} from '@/lib/paged-resource'
import {keyedStaleGuard} from '@/lib/stale-guard'
import {registerReset} from '@/lib/store-reset'

// a per-id cache of paged lists: the keyed analogue of createPagedStore, for
// lists scoped to an entity (a place's crumbs, a profile's crumbs/trails).
// each id loads and pages independently with its own stale guard
type KeyedPaged<T> = {
  byId: Record<string, PagedSlice<T>>
}

const emptyKeyedPaged = {byId: {}} satisfies KeyedPaged<never>

// stable empty array so a missing slice's selector returns a referentially
// equal value across renders (a fresh [] would loop useSyncExternalStore)
const EMPTY = Object.freeze([]) as never[]

const keyedItems = <T>(state: KeyedPaged<T>, id: string): T[] =>
  state.byId[id]?.items ?? EMPTY

type KeyedPagedStore<T> = KeyedPaged<T> & {
  load: (id: string) => Promise<void>
  loadMore: (id: string) => Promise<void>
}

// build a keyed-paged store from a per-id page fetcher: owns the seq map, the
// load/loadMore actions and the id-scoped selectors, so each caller collapses
// to one factory call + re-exported hooks. an optional snapshot makes it
// offline-first: byId seeds from disk (lru-capped) and every commit persists back
export const createKeyedPagedStore = <T>(
  fetchPage: (id: string) => PageFetcher<T>,
  snapshot?: KeyedSnapshot<PagedSlice<T>>,
) => {
  const guard = keyedStaleGuard()
  const seeded = snapshot?.seed()
  const store = create<KeyedPagedStore<T>>((set, get) => {
    const run = (id: string, mode: 'refresh' | 'more') => {
      const current = get().byId[id] ?? emptyPaged<T>()
      if (mode === 'more' && !canLoadMore(current))
        return Promise.resolve()
      const token = guard.claim(id)
      return stepPaged(
        current,
        fetchPage(id),
        mode,
        (patch) =>
          set((s) => {
            const slice = s.byId[id] ?? emptyPaged<T>()
            return {
              byId: {...s.byId, [id]: {...slice, ...patch(slice)}},
            }
          }),
        () => !guard.isCurrent(id, token),
      )
    }
    return {
      ...emptyKeyedPaged,
      ...(seeded && {byId: seeded}),
      load: (id) => run(id, 'refresh'),
      loadMore: (id) => run(id, 'more'),
    }
  })
  if (snapshot)
    store.subscribe((state, prev) => {
      if (state.byId !== prev.byId)
        snapshot.sync(state.byId, prev.byId)
    })
  registerReset(() => {
    guard.invalidate()
    // setState first: the persist subscriber runs, then clear wins
    store.setState(emptyKeyedPaged)
    snapshot?.clear()
  })

  // drop one id's slice entirely (its entity was deleted); claiming the
  // guard fences an in-flight load that would otherwise resurrect it
  const dropId = (id: string) => {
    guard.claim(id)
    store.setState((s) => {
      if (!(id in s.byId)) return s
      const {[id]: _dropped, ...byId} = s.byId
      return {byId}
    })
  }

  const patchSlices = (patch: (items: T[]) => T[]) =>
    store.setState((s) => ({
      byId: Object.fromEntries(
        Object.entries(s.byId).map(([id, slice]) => [
          id,
          {...slice, items: patch(slice.items)},
        ]),
      ),
    }))

  // merge newly-arrived items ahead of a slice, skipping ones it already
  // holds — the polling/send primitive for feeds that grow at the head
  const prependItems = (
    id: string,
    items: T[],
    idOf: (item: T) => string,
  ) =>
    store.setState((s) => {
      const slice = s.byId[id] ?? emptyPaged<T>()
      const seen = new Set(slice.items.map(idOf))
      const fresh = items.filter((item) => !seen.has(idOf(item)))
      if (!fresh.length) return s
      return {
        byId: {
          ...s.byId,
          [id]: {...slice, items: [...fresh, ...slice.items]},
        },
      }
    })

  return {
    useItems: (id: string) => store((s) => keyedItems(s, id)),
    getItems: (id: string) => keyedItems(store.getState(), id),
    useLoading: (id: string) =>
      store((s) => s.byId[id]?.loading ?? false),
    useLoadingMore: (id: string) =>
      store((s) => s.byId[id]?.loadingMore ?? false),
    useError: (id: string) => store((s) => s.byId[id]?.error ?? null),
    useReachedEnd: (id: string) =>
      store((s) => s.byId[id]?.reachedEnd ?? false),
    useLoad: () => store((s) => s.load),
    useLoadMore: () => store((s) => s.loadMore),
    load: (id: string) => store.getState().load(id),
    prependItems,
    dropId,
    // entity-sync hooks: replace or drop copies across every loaded slice
    mapItems: (fn: (item: T) => T) =>
      patchSlices((items) => items.map(fn)),
    removeItem: (predicate: (item: T) => boolean) =>
      patchSlices((items) => items.filter((i) => !predicate(i))),
  }
}
