package search import ( "context" "fmt" "log" "sync" ) // SearchResult represents a single web search result. type SearchResult struct { Title string `json:"title"` URL string `json:"url"` Snippet string `json:"snippet"` } // Provider is the interface for web search backends. type Provider interface { // Search performs a web search and returns up to maxResults results. Search(ctx context.Context, query string, maxResults int) ([]SearchResult, error) // Name returns the provider identifier (e.g. "duckduckgo", "searxng"). Name() string } // ── Provider Registry ─────────────────────── var ( mu sync.RWMutex providers = map[string]Provider{} active string ) // RegisterProvider adds a search provider to the registry. func RegisterProvider(p Provider) { mu.Lock() defer mu.Unlock() providers[p.Name()] = p log.Printf("🔍 Registered search provider: %s", p.Name()) } // SetActive sets the active search provider by name. func SetActive(name string) error { mu.Lock() defer mu.Unlock() if _, ok := providers[name]; !ok { return fmt.Errorf("unknown search provider: %s", name) } active = name log.Printf("🔍 Active search provider: %s", name) return nil } // Active returns the currently active provider, or nil if none configured. func Active() Provider { mu.RLock() defer mu.RUnlock() if active == "" { return nil } return providers[active] } // Available returns the names of all registered providers. func Available() []string { mu.RLock() defer mu.RUnlock() names := make([]string, 0, len(providers)) for name := range providers { names = append(names, name) } return names } // Config holds the persisted search configuration. type Config struct { Provider string `json:"provider"` // "duckduckgo" or "searxng" Endpoint string `json:"endpoint"` // SearXNG endpoint URL APIKey string `json:"api_key"` // SearXNG API key (optional) MaxResults int `json:"max_results"` // default results per search } // DefaultConfig returns the default search config (DuckDuckGo, no key needed). func DefaultConfig() Config { return Config{Provider: "duckduckgo", MaxResults: 5} } // ApplyConfig applies a Config to the active provider registry. // Called on startup and when admin saves settings. func ApplyConfig(cfg Config) error { if cfg.Provider == "" { cfg.Provider = "duckduckgo" } // Configure SearXNG if selected mu.RLock() if sxng, ok := providers["searxng"].(*SearXNG); ok { sxng.Configure(cfg.Endpoint, cfg.APIKey) } mu.RUnlock() return SetActive(cfg.Provider) }