import {removeById, replaceById} from '@crumbs/core/collections'
import {DEFAULT_PAGE_SIZE} from '@crumbs/core/schemas'
import {create} from 'zustand'

import {cleanError} from '@/lib/errors'
import {
  arrayOf,
  type Parse,
  type Snapshot,
} from '@/lib/snapshot-cache'
import {staleGuard} from '@/lib/stale-guard'
import {registerReset} from '@/lib/store-reset'

// the cursor-paged analogue of the slot machine: one state machine for every
// infinite list (feed, friends, trails, per-id lists). owns loading flags,
// append-vs-replace, end detection and a stale-response guard so a refresh
// landing after a newer request can never clobber it

export type PagedSlice<T> = {
  items: T[]
  cursor: string | null
  reachedEnd: boolean
  loading: boolean
  loadingMore: boolean
  error: string | null
}

export const emptyPaged = <T>(): PagedSlice<T> => ({
  items: [],
  cursor: null,
  reachedEnd: false,
  loading: false,
  loadingMore: false,
  error: null,
})

export type Page<T> = {items: T[]; nextCursor: string | null}

// fetch one page; cursor is undefined for the first page
export type PageFetcher<T> = (cursor?: string) => Promise<Page<T>>

export const PAGE_SIZE = DEFAULT_PAGE_SIZE

// the query params every cursor-paged fetcher sends
export const pageParams = (cursor?: string) => ({
  limit: String(PAGE_SIZE),
  ...(cursor ? {cursor} : {}),
})

type SlicePatch<T> = (s: PagedSlice<T>) => Partial<PagedSlice<T>>

// a loadMore is a no-op unless a page is settled, more remain, and nothing is
// already in flight. checked by the caller *before* it claims a stale-guard
// token, so a spurious onEndReached can't invalidate an in-flight refresh
export const canLoadMore = (s: PagedSlice<unknown>): boolean =>
  !s.loading && !s.loadingMore && !s.reachedEnd && s.cursor != null

// run one load step. apply takes a pure updater so every commit reads the
// latest slice — an append must not stomp a concurrent prepend. isStale lets
// a newer request cancel this one's commit
export const stepPaged = async <T>(
  current: PagedSlice<T>,
  fetchPage: PageFetcher<T>,
  mode: 'refresh' | 'more',
  apply: (patch: SlicePatch<T>) => void,
  isStale: () => boolean,
): Promise<void> => {
  apply(() =>
    mode === 'more'
      ? {loadingMore: true, error: null}
      : {loading: true, error: null},
  )

  try {
    const {items, nextCursor} = await fetchPage(
      mode === 'more' ? (current.cursor ?? undefined) : undefined,
    )
    if (isStale()) return
    apply((s) => ({
      items: mode === 'more' ? [...s.items, ...items] : items,
      cursor: nextCursor,
      reachedEnd: nextCursor == null,
      loading: false,
      loadingMore: false,
    }))
  } catch (err) {
    if (isStale()) return
    apply(() => ({
      loading: false,
      loadingMore: false,
      error: cleanError(err),
    }))
  }
}

export type PagedStore<T> = PagedSlice<T> & {
  load: () => Promise<void>
  loadMore: () => Promise<void>
  reset: () => void
  prepend: (item: T) => void
  replace: (id: string, item: T) => void
  removeItem: (id: string) => void
}

// the selector-hook set every paged store consumer re-exports, destructured
// with renames so each store keeps its own vocabulary
export const pagedHooks = <T>(
  store: <U>(selector: (s: PagedStore<T>) => U) => U,
) => ({
  useItems: () => store((s) => s.items),
  useLoading: () => store((s) => s.loading),
  useLoadingMore: () => store((s) => s.loadingMore),
  useReachedEnd: () => store((s) => s.reachedEnd),
  useError: () => store((s) => s.error),
  useLoad: () => store((s) => s.load),
  useLoadMore: () => store((s) => s.loadMore),
})

// what a paged store persists: cursor + reachedEnd travel with the items so
// canLoadMore stays honest after a rehydrate
export type PagedSnapshot<T> = Pick<
  PagedSlice<T>,
  'items' | 'cursor' | 'reachedEnd'
>

// past three pages the disk cost outgrows the offline value
export const SNAPSHOT_MAX_ITEMS = PAGE_SIZE * 3

// a capped list drops its cursor: the tail past the cap is gone, so resuming
// pagination from the old cursor would leave a gap. a refresh restores both
export const toSnapshot = <T>(
  s: PagedSnapshot<T>,
): PagedSnapshot<T> =>
  s.items.length > SNAPSHOT_MAX_ITEMS
    ? {
        items: s.items.slice(0, SNAPSHOT_MAX_ITEMS),
        cursor: null,
        reachedEnd: false,
      }
    : {items: s.items, cursor: s.cursor, reachedEnd: s.reachedEnd}

export const parsePagedSnapshot =
  <T>(parseItem: Parse<T>): Parse<PagedSnapshot<T>> =>
  (raw) => {
    if (typeof raw !== 'object' || raw == null) return undefined
    const {items, cursor, reachedEnd} = raw as Partial<
      Record<keyof PagedSnapshot<unknown>, unknown>
    >
    if (cursor !== null && typeof cursor !== 'string')
      return undefined
    if (typeof reachedEnd !== 'boolean') return undefined
    const parsed = arrayOf(parseItem)(items)
    return parsed && {items: parsed, cursor, reachedEnd}
  }

// a standalone store for a single global paged list. mutation helpers keep
// the loaded page in sync with optimistic add/edit/delete. an optional
// snapshot makes it offline-first: state seeds from disk before the first
// render and every items commit persists back
export const createPagedStore = <T extends {id: string}>(
  fetchPage: PageFetcher<T>,
  snapshot?: Snapshot<PagedSnapshot<T>>,
) => {
  const guard = staleGuard()
  const seeded = snapshot?.read()
  const store = create<PagedStore<T>>((set, get) => {
    const run = (mode: 'refresh' | 'more') => {
      const current = get()
      if (mode === 'more' && !canLoadMore(current))
        return Promise.resolve()
      const token = guard.claim()
      return stepPaged(
        current,
        fetchPage,
        mode,
        (patch) => set((s) => patch(s)),
        () => !guard.isCurrent(token),
      )
    }
    return {
      ...emptyPaged<T>(),
      // re-cap on seed: an older write may exceed the current cap
      ...(seeded ? toSnapshot(seeded) : undefined),
      load: () => run('refresh'),
      loadMore: () => run('more'),
      reset: () => {
        guard.invalidate()
        // setState first: the persist subscriber runs, then clear wins
        set(emptyPaged<T>())
        snapshot?.clear()
      },
      prepend: (item) => set((s) => ({items: [item, ...s.items]})),
      replace: (id, item) =>
        set((s) => ({items: replaceById(s.items, id, item)})),
      removeItem: (id) =>
        set((s) => ({items: removeById(s.items, id)})),
    }
  })
  if (snapshot)
    store.subscribe((state, prev) => {
      if (
        state.items !== prev.items ||
        state.cursor !== prev.cursor ||
        state.reachedEnd !== prev.reachedEnd
      )
        snapshot.write(toSnapshot(state))
    })
  registerReset(() => store.getState().reset())
  return store
}
