"use client" import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react" import { useDataSource } from "@/lib/data-source" import type { Domain, IpRange, Asn } from "@/lib/data" import { ApiClientError, requestJson } from "@/shared/api/http-client" export interface EvoBgpCommunityRow { id: string value: string name: string description: string type: "standard" | "no-export" | "no-advertise" | "local-as" | "custom" filterIds: string[] serverCount: number prefixCount: number action: "permit" | "deny" | "local-pref" | "metric" actionValue?: number enabled: boolean } export interface EvoBgpCatalogSnapshot { fetchedAt: string modules: { id: string; name: string; type: string }[] domains: Domain[] ipRanges: IpRange[] asns: Asn[] communities: EvoBgpCommunityRow[] } export interface EvoBgpSettingsDto { baseUrl: string enabled: boolean secretConfigured: boolean } /** undefined — не менять ключ; null или очистка через отдельное сохранение */ export type EvoBgpSavePayload = { baseUrl?: string enabled?: boolean apiKey?: string | null } /** Черновик для POST /evobgp/test: пустой объект — всё из БД */ export type EvoBgpTestDraft = { baseUrl?: string; apiKey?: string } interface EvoBgpContextValue { baseUrl: string enabled: boolean secretConfigured: boolean settingsLoaded: boolean snapshot: EvoBgpCatalogSnapshot | null loading: boolean error: string | null loadSettings: () => Promise saveSettings: (patch: EvoBgpSavePayload) => Promise refresh: () => Promise testConnection: (draft?: EvoBgpTestDraft) => Promise<{ ok: boolean; message: string }> } const EvoBgpContext = createContext(null) function errorMessage(e: unknown, fallback: string): string { if (e instanceof ApiClientError) return e.message || fallback if (e instanceof Error) return e.message || fallback return fallback } export function EvoBGPProvider({ children }: { children: React.ReactNode }) { const { mode, backendUrl, backendStatus } = useDataSource() const [baseUrl, setBaseUrlState] = useState("") const [enabled, setEnabledState] = useState(false) const [secretConfigured, setSecretConfigured] = useState(false) const [settingsLoaded, setSettingsLoaded] = useState(false) const [snapshot, setSnapshot] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const pullCatalog = useCallback( async (catalogEnabled: boolean) => { if (mode !== "live" || backendStatus !== true || !catalogEnabled) { setSnapshot(null) setError(null) return } setLoading(true) setError(null) try { const data = await requestJson( backendUrl, "/api/evobgp/catalog", { method: "POST" }, ) setSnapshot(data) } catch (e) { setSnapshot(null) setError(errorMessage(e, "Ошибка загрузки")) } finally { setLoading(false) } }, [mode, backendStatus, backendUrl], ) const loadSettings = useCallback(async () => { if (mode !== "live" || backendStatus !== true) { setSettingsLoaded(false) setSnapshot(null) setError(null) return } try { const data = await requestJson( backendUrl, "/api/evobgp/settings", ) setBaseUrlState(data.baseUrl ?? "") setEnabledState(Boolean(data.enabled)) setSecretConfigured(Boolean(data.secretConfigured)) setSettingsLoaded(true) setError(null) await pullCatalog(Boolean(data.enabled)) } catch (e) { setSettingsLoaded(true) setError(errorMessage(e, "Не удалось загрузить настройки EvoBGP")) } }, [mode, backendStatus, backendUrl, pullCatalog]) useEffect(() => { queueMicrotask(() => { void loadSettings() }) }, [loadSettings]) const saveSettings = useCallback( async (patch: EvoBgpSavePayload) => { const data = await requestJson( backendUrl, "/api/evobgp/settings", { method: "PUT", body: JSON.stringify(patch), }, ) const nextEnabled = Boolean(data.enabled) setBaseUrlState(data.baseUrl ?? "") setEnabledState(nextEnabled) setSecretConfigured(Boolean(data.secretConfigured)) setError(null) // Каталог не должен ронять успех сохранения (401/502 на catalog ≠ «настройки не сохранились») try { await pullCatalog(nextEnabled) } catch { /* pullCatalog already sets error state */ } }, [backendUrl, pullCatalog], ) const refresh = useCallback(async () => { await pullCatalog(enabled) }, [enabled, pullCatalog]) const testConnection = useCallback( async (draft?: EvoBgpTestDraft) => { try { await requestJson<{ ok?: boolean }>(backendUrl, "/api/evobgp/test", { method: "POST", body: JSON.stringify(draft ?? {}), }) return { ok: true, message: "Соединение с EvoBGP установлено" } } catch (e) { return { ok: false, message: errorMessage(e, "Ошибка") } } }, [backendUrl], ) const value = useMemo( () => ({ baseUrl, enabled, secretConfigured, settingsLoaded, snapshot, loading, error, loadSettings, saveSettings, refresh, testConnection, }), [ baseUrl, enabled, secretConfigured, settingsLoaded, snapshot, loading, error, loadSettings, saveSettings, refresh, testConnection, ], ) return {children} } export function useEvoBGP(): EvoBgpContextValue { const ctx = useContext(EvoBgpContext) if (!ctx) throw new Error("useEvoBGP must be used within EvoBGPProvider") return ctx }