import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import type { ApiError } from '../types' import { endpoints } from '../api/client' import { useDataLanguage } from '../lib/DataLanguageContext' export interface LinkRule { id: string action: 'include' | 'exclude' scope: string termId?: number | null termName?: string | null postId?: number | null postTitle?: string | null } export interface ScopeOption { value: string group: 'general' | 'content_types' | 'taxonomies' | 'archive_pages' | 'specific_items' | string kind: 'all' | 'post_type' | 'taxonomy' | 'archive' | 'post' | string label: string objectLabel?: string needsTerm: boolean needsPost?: boolean multiSelect?: boolean taxonomy?: string postType?: string } export interface TaxonomyTerm { id: number name: string slug: string count: number } export interface ScopePostItem { id: number title: string slug: string url: string postType: string } export interface LinkCollectResult { added: number updated: number removed: number total: number } interface AsyncState { data: T loading: boolean error: ApiError | null } let ruleCounter = 0 export function createEmptyLinkRule(): LinkRule { return { id: `link-rule-${Date.now()}-${++ruleCounter}`, action: 'include', scope: 'all', termId: null, termName: null, postId: null, postTitle: null, } } export function normalizeLinkRules(rules: LinkRule[] | null | undefined): LinkRule[] { const normalized: LinkRule[] = Array.isArray(rules) ? rules.map((rule): LinkRule => ({ id: rule.id || createEmptyLinkRule().id, action: rule.action === 'exclude' ? 'exclude' : 'include', scope: rule.scope || 'all', termId: rule.termId ?? null, termName: rule.termName ?? null, postId: rule.postId ?? null, postTitle: rule.postTitle ?? null, })) : [] return normalized.length > 0 ? normalized : [createEmptyLinkRule()] } export function areLinkRulesEqual(left: LinkRule[] | null | undefined, right: LinkRule[] | null | undefined): boolean { const stripIds = (rules: LinkRule[]) => rules.map(({ id: _id, ...rest }) => rest) return JSON.stringify(stripIds(normalizeLinkRules(left))) === JSON.stringify(stripIds(normalizeLinkRules(right))) } export function useLinkRules() { const { dataLanguage } = useDataLanguage() const [state, setState] = useState>({ data: [createEmptyLinkRule()], loading: true, error: null, }) const load = useCallback(async () => { setState(prev => ({ ...prev, loading: true, error: null })) try { const response = await endpoints.getLinkRules() setState({ data: normalizeLinkRules(response.data as LinkRule[]), loading: false, error: null, }) } catch (error) { setState({ data: [createEmptyLinkRule()], loading: false, error: error as ApiError, }) } }, []) useEffect(() => { void load() }, [dataLanguage, load]) return useMemo(() => ({ rules: state.data, loading: state.loading, error: state.error, reload: load, }), [load, state.data, state.error, state.loading]) } export function useScopeOptions() { const [state, setState] = useState>({ data: [], loading: true, error: null, }) const load = useCallback(async () => { setState(prev => ({ ...prev, loading: true, error: null })) try { const response = await endpoints.getLinkScopeOptions() setState({ data: Array.isArray(response.data) ? response.data as ScopeOption[] : [], loading: false, error: null, }) } catch (error) { setState({ data: [], loading: false, error: error as ApiError, }) } }, []) useEffect(() => { void load() }, [load]) return useMemo(() => ({ scopeOptions: state.data, loading: state.loading, error: state.error, reload: load, }), [load, state.data, state.error, state.loading]) } export function useTaxonomyTerms(taxonomy: string | null | undefined) { const { dataLanguage } = useDataLanguage() const cacheRef = useRef>({}) const [state, setState] = useState>({ data: [], loading: false, error: null, }) const load = useCallback(async () => { if (!taxonomy) { setState({ data: [], loading: false, error: null }) return } const cacheKey = `${taxonomy}:${dataLanguage}` if (cacheRef.current[cacheKey]) { setState({ data: cacheRef.current[cacheKey], loading: false, error: null }) return } setState(prev => ({ ...prev, loading: true, error: null })) try { const response = await endpoints.getLinkTerms(taxonomy) const data = Array.isArray(response.data) ? response.data as TaxonomyTerm[] : [] cacheRef.current[cacheKey] = data setState({ data, loading: false, error: null }) } catch (error) { setState({ data: [], loading: false, error: error as ApiError }) } }, [taxonomy, dataLanguage]) useEffect(() => { void load() }, [load]) return useMemo(() => ({ terms: state.data, loading: state.loading, error: state.error, reload: load, }), [load, state.data, state.error, state.loading]) } export function useScopePosts(postType: string | null | undefined, search: string, limit = 20) { const { dataLanguage } = useDataLanguage() const [state, setState] = useState>({ data: [], loading: false, error: null, }) useEffect(() => { if (!postType) { setState({ data: [], loading: false, error: null }) return } let cancelled = false const timer = window.setTimeout(async () => { setState(prev => ({ ...prev, loading: true, error: null })) try { const response = await endpoints.getLinkPosts(postType, search, limit) if (cancelled) return setState({ data: Array.isArray(response.data) ? response.data as ScopePostItem[] : [], loading: false, error: null, }) } catch (error) { if (cancelled) return setState({ data: [], loading: false, error: error as ApiError, }) } }, 250) return () => { cancelled = true window.clearTimeout(timer) } }, [limit, postType, search, dataLanguage]) return useMemo(() => ({ posts: state.data, loading: state.loading, error: state.error, }), [state.data, state.error, state.loading]) } export function useScopePostsPicker(postType: string | null | undefined, initialLimit = 50, step = 50) { const { dataLanguage } = useDataLanguage() const [search, setSearch] = useState('') const [limit, setLimit] = useState(initialLimit) const [state, setState] = useState>({ data: [], loading: false, error: null, }) useEffect(() => { setLimit(initialLimit) }, [initialLimit, postType, search]) useEffect(() => { if (!postType) { setState({ data: [], loading: false, error: null }) return } let cancelled = false const timer = window.setTimeout(async () => { setState(prev => ({ ...prev, loading: true, error: null })) try { const response = await endpoints.getLinkPosts(postType, search, limit) if (cancelled) return const data = Array.isArray(response.data) ? response.data as ScopePostItem[] : [] setState({ data, loading: false, error: null, }) } catch (error) { if (cancelled) return setState({ data: [], loading: false, error: error as ApiError, }) } }, 250) return () => { cancelled = true window.clearTimeout(timer) } }, [limit, postType, search, dataLanguage]) const loadMore = useCallback(() => { setLimit(prev => prev + step) }, [step]) return useMemo(() => ({ posts: state.data, loading: state.loading, error: state.error, search, setSearch, hasMore: state.data.length >= limit, loadMore, }), [limit, loadMore, search, state.data, state.error, state.loading]) } export function useCollectLinkRules() { const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const collect = useCallback(async (rules: LinkRule[]) => { setLoading(true) setError(null) try { const response = await endpoints.collectLinks(rules) return response.data as LinkCollectResult } catch (err) { const apiError = err as ApiError setError(apiError) throw apiError } finally { setLoading(false) } }, []) return { collect, loading, error } } export function useSaveLinkRules() { const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const save = useCallback(async (rules: LinkRule[]) => { setLoading(true) setError(null) try { const response = await endpoints.saveLinkRules(rules) return normalizeLinkRules(response.data as LinkRule[]) } catch (err) { const apiError = err as ApiError setError(apiError) throw apiError } finally { setLoading(false) } }, []) return { save, loading, error } } export function useDeleteLinkIndex() { const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const remove = useCallback(async () => { setLoading(true) setError(null) try { const response = await endpoints.deleteLinkIndex() return response.data as { deleted: number } } catch (err) { const apiError = err as ApiError setError(apiError) throw apiError } finally { setLoading(false) } }, []) return { remove, loading, error } }