import {
  patchById,
  removeById,
  replaceById,
} from '@crumbs/core/collections'
import type {Crumb, PublicProfile, Trail} from '@crumbs/core/schemas'
import {create} from 'zustand'

import {
  crumbDropped,
  crumbSynced,
  trailDropped,
  trailPatched,
} from '@/lib/entity-sync'
import {cleanError, unwrap} from '@/lib/errors'
import {rpc} from '@/lib/rpc'
import {staleGuard} from '@/lib/stale-guard'
import {registerReset} from '@/lib/store-reset'

const DEBOUNCE_MS = 300

type SearchState = {
  crumbs: Crumb[]
  trails: Trail[]
  users: PublicProfile[]
  loading: boolean
  error: string | null
  search: (q: string) => void
  clear: () => void
}

let timer: ReturnType<typeof setTimeout> | undefined
// drop responses from stale (superseded) queries
const guard = staleGuard()

const initialState = {
  crumbs: [],
  trails: [],
  users: [],
  loading: false,
  error: null,
} satisfies Omit<SearchState, 'search' | 'clear'>

const useSearchStore = create<SearchState>((set) => ({
  ...initialState,
  search: (q) => {
    if (timer) clearTimeout(timer)
    const query = q.trim()
    if (!query) {
      guard.invalidate()
      set(initialState)
      return
    }
    set({loading: true, error: null})
    // claim before the debounce so a newer search supersedes an older one
    // still awaiting its response, not just one that has already fired
    const token = guard.claim()
    timer = setTimeout(async () => {
      try {
        const {crumbs, trails, users} = await unwrap(
          rpc.search.$get({query: {q: query}}),
        )
        if (!guard.isCurrent(token)) return
        set({crumbs, trails, users, loading: false})
      } catch (err) {
        if (!guard.isCurrent(token)) return
        set({loading: false, error: cleanError(err)})
      }
    }, DEBOUNCE_MS)
  },
  clear: () => {
    if (timer) clearTimeout(timer)
    guard.invalidate()
    set(initialState)
  },
}))

registerReset(() => useSearchStore.getState().clear())

crumbSynced.subscribe((crumb) =>
  useSearchStore.setState((s) => ({
    crumbs: replaceById(s.crumbs, crumb.id, crumb),
  })),
)
crumbDropped.subscribe((id) =>
  useSearchStore.setState((s) => ({
    crumbs: removeById(s.crumbs, id),
  })),
)
trailPatched.subscribe(({id, patch}) =>
  useSearchStore.setState((s) => ({
    trails: patchById(s.trails, id, patch),
  })),
)
trailDropped.subscribe((id) =>
  useSearchStore.setState((s) => ({
    trails: removeById(s.trails, id),
  })),
)

export const useSearchCrumbs = () => useSearchStore((s) => s.crumbs)
export const useSearchTrails = () => useSearchStore((s) => s.trails)
export const useSearchUsers = () => useSearchStore((s) => s.users)
export const useSearchLoading = () => useSearchStore((s) => s.loading)
export const useSearchError = () => useSearchStore((s) => s.error)
export const useSearch = () => useSearchStore((s) => s.search)
