import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { distanceInKm } from '@/lib/utils' import type { ApiDensityCell, ApiPoint, ApiSnapshot, FeedStatus, LatLng } from '@/types/api' const API_BASE = import.meta.env.VITE_API_BASE ?? 'http://localhost:8000/api/signals' const SNAPSHOT_LIMIT = 750 const DEFAULT_REFRESH_MS = 7000 const VISIBLE_RADIUS_KM = 1 interface UseHotspotFeedOptions { autoRefreshMs?: number } interface SubmitResult { success: boolean } export interface FeedError { key: string values?: Record } export function useHotspotFeed({ autoRefreshMs = DEFAULT_REFRESH_MS }: UseHotspotFeedOptions = {}) { const [status, setStatus] = useState('loading') const [error, setError] = useState(null) const [rawPoints, setRawPoints] = useState([]) const [rawDensity, setRawDensity] = useState([]) const [rawLatestByUser, setRawLatestByUser] = useState([]) const [clientKey, setClientKey] = useState(null) const [lastUpdated, setLastUpdated] = useState(null) const statusRef = useRef('loading') const initialLoadRef = useRef(true) const setStatusSafe = useCallback((next: FeedStatus) => { statusRef.current = next setStatus(next) }, []) const fetchSnapshot = useCallback( async (options?: { silent?: boolean }) => { const silent = options?.silent ?? false const previousStatus = statusRef.current const isInitial = initialLoadRef.current if (previousStatus !== 'posting') { if (isInitial) { setStatusSafe('loading') } else if (!silent) { setStatusSafe('refreshing') } } try { const response = await fetch(`${API_BASE}?limit=${SNAPSHOT_LIMIT}`, { cache: 'no-store' }) if (!response.ok) { throw new Error('feed-unavailable') } const data: ApiSnapshot = await response.json() setRawPoints(data.points ?? []) setRawDensity(data.density ?? []) setRawLatestByUser(data.latestByUser ?? []) setClientKey(data.clientKey ?? null) setLastUpdated(data.updatedAt ?? new Date().toISOString()) setError(null) initialLoadRef.current = false const nextStatus = previousStatus === 'posting' ? 'posting' : 'idle' setStatusSafe(nextStatus) } catch (error) { const message = error instanceof Error ? error.message : null const key = message === 'feed-unavailable' ? 'errors.feedUnavailable' : 'errors.feedUnknown' setError({ key }) if (initialLoadRef.current) { setStatusSafe('error') } else if (previousStatus !== 'posting') { setStatusSafe('idle') } } }, [setStatusSafe], ) useEffect(() => { fetchSnapshot().catch(() => undefined) if (!autoRefreshMs) { return } const interval = window.setInterval(() => { fetchSnapshot({ silent: true }).catch(() => undefined) }, autoRefreshMs) return () => { window.clearInterval(interval) } }, [autoRefreshMs, fetchSnapshot]) const submitPoint = useCallback( async (lat: number, lng: number): Promise => { setStatusSafe('posting') try { const response = await fetch(API_BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lat, lng }), }) if (!response.ok) { const payload = await response.json().catch(() => null) const message = payload?.message as string | undefined if (message) { setError({ key: 'errors.submitWithReason', values: { message } }) } else { setError({ key: 'errors.submitUnavailable' }) } setStatusSafe('error') return { success: false } } await fetchSnapshot({ silent: true }) setError(null) setStatusSafe('idle') return { success: true } } catch (error) { const message = error instanceof Error ? error.message : null if (message) { setError({ key: 'errors.submitWithReason', values: { message } }) } else { setError({ key: 'errors.submitUnknown' }) } setStatusSafe('error') return { success: false } } }, [fetchSnapshot, setStatusSafe], ) const hasClientKey = Boolean(clientKey) const filterWithinRadius = useCallback( (collection: T[], origin: LatLng | null) => { if (!origin) { return [] } return collection.filter((item) => distanceInKm(origin, item) <= VISIBLE_RADIUS_KM) }, [], ) const selectVisibleDensity = useCallback( (origin: LatLng | null) => filterWithinRadius(rawDensity, origin), [filterWithinRadius, rawDensity], ) const selectVisiblePoints = useCallback( (origin: LatLng | null) => filterWithinRadius(rawPoints, origin), [filterWithinRadius, rawPoints], ) const selectVisibleLatestByUser = useCallback( (origin: LatLng | null) => filterWithinRadius(rawLatestByUser, origin), [filterWithinRadius, rawLatestByUser], ) const myLatestPoint = useMemo(() => { if (!hasClientKey) { return null } return rawLatestByUser.find((point) => point.userKey === clientKey) ?? null }, [clientKey, hasClientKey, rawLatestByUser]) return { status, error, submitPoint, fetchSnapshot, rawDensity, rawPoints, rawLatestByUser, clientKey, lastUpdated, hasClientKey, selectVisibleDensity, selectVisiblePoints, selectVisibleLatestByUser, myLatestPoint, } }