import {create} from 'zustand'

import {runSlot, type Slot} from '@/lib/async-slot'
import {type KeyedSnapshot} from '@/lib/keyed-snapshot'
import {
  keyedStaleGuard,
  type KeyedStaleGuard,
} from '@/lib/stale-guard'
import {registerReset} from '@/lib/store-reset'

// a per-id detail cache: each record loaded independently in its own slot.
// data is honestly T | undefined — absent until the first successful load —
// so consumers are forced through the ?.data they already use
export type KeyedDetail<T> = {
  byId: Record<string, Slot<T | undefined>>
}

export const emptyKeyedDetail = {
  byId: {},
} satisfies KeyedDetail<never>

type KeyedSetter<T> = (
  patch: (state: KeyedDetail<T>) => Partial<KeyedDetail<T>>,
) => void

// merge a resolved value into an id's slot; the optimistic-mutation companion
// to loadKeyed for stores that patch detail after a write
export const setDetail = <T>(
  byId: KeyedDetail<T>['byId'],
  id: string,
  data: T,
): KeyedDetail<T>['byId'] => ({
  ...byId,
  [id]: {data, loading: false, error: null},
})

// patch a loaded id's data in place; an absent id (or a patch that changes
// nothing) returns the same byId so nothing re-renders or re-persists
export const patchData = <T>(
  byId: KeyedDetail<T>['byId'],
  id: string,
  fn: (data: T) => T,
): KeyedDetail<T>['byId'] => {
  const data = byId[id]?.data
  if (data === undefined) return byId
  const next = fn(data)
  return next === data ? byId : setDetail(byId, id, next)
}

// the selector-hook set every keyed-detail store consumer re-exports,
// destructured with renames so each store keeps its own vocabulary
export const keyedDetailHooks = <T>(
  store: <U>(selector: (s: KeyedDetail<T>) => U) => U,
) => ({
  useData: (id: string) => store((s) => s.byId[id]?.data),
  useLoading: (id: string) =>
    store((s) => s.byId[id]?.loading ?? false),
  useError: (id: string) => store((s) => s.byId[id]?.error ?? null),
})

// wire a hand-built keyed-detail store (crumbs, trails, profile) to its
// snapshot. seeding stays in the store initializer (byId: snapshot.seed());
// this adds persist-on-change plus the privacy-seam reset in its one safe
// order — invalidate in-flight loads, empty byId so the subscriber persists the
// empty, then clear disk — the same contract the slot/paged factories apply
export const persistKeyedDetail = <T, S extends KeyedDetail<T>>(
  store: {
    subscribe: (listener: (state: S, prev: S) => void) => unknown
    setState: (partial: Pick<S, 'byId'>) => void
  },
  snapshot: KeyedSnapshot<Slot<T | undefined>>,
  guard: Pick<KeyedStaleGuard, 'invalidate'>,
): void => {
  store.subscribe((state, prev) => {
    if (state.byId !== prev.byId) snapshot.sync(state.byId, prev.byId)
  })
  registerReset(() => {
    guard.invalidate()
    store.setState({byId: {}})
    snapshot.clear()
  })
}

export type KeyedDetailStore<T> = KeyedDetail<T> & {
  load: (id: string) => Promise<void>
}

// a whole per-id detail store: guard + optional snapshot seed + the keyed
// load + persist/reset wiring in one call — for stores whose mutations live
// outside the state object (module fns over setState). stores with embedded
// mutations keep hand-building over the same pieces
export const createKeyedDetailStore = <T>(
  fetcher: (id: string) => Promise<T>,
  snapshot?: KeyedSnapshot<Slot<T | undefined>>,
) => {
  const guard = keyedStaleGuard()
  const seeded = snapshot?.seed()
  const store = create<KeyedDetailStore<T>>((set) => ({
    ...emptyKeyedDetail,
    ...(seeded && {byId: seeded}),
    load: (id) => loadKeyed(set, guard, id, () => fetcher(id)),
  }))
  if (snapshot) persistKeyedDetail(store, snapshot, guard)
  else
    registerReset(() => {
      guard.invalidate()
      store.setState({byId: {}})
    })
  return store
}

// thread one keyed load through its slot's loading → data / error; the guard
// is per-id so id A's refresh never invalidates id B's
export const loadKeyed = <T>(
  set: KeyedSetter<T>,
  guard: KeyedStaleGuard,
  id: string,
  fetcher: () => Promise<T>,
): Promise<void> =>
  runSlot<T | undefined>(
    (patch) =>
      set((s) => {
        const slot = s.byId[id] ?? {
          data: undefined,
          loading: false,
          error: null,
        }
        return {byId: {...s.byId, [id]: {...slot, ...patch}}}
      }),
    fetcher,
    {
      claim: () => guard.claim(id),
      isCurrent: (token) => guard.isCurrent(id, token),
    },
  )
