package runtime

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

// WebSearchProvider searches public web pages for Knowledge Q&A web navigation.
// Search results are not citable evidence; knowledge_web_read must read a
// returned URL before the model can cite the page.
type WebSearchProvider interface {
	Search(ctx context.Context, query string, limit int) ([]WebSearchResult, error)
}

// WebSearchResult is the minimal search result shape used by the MVP.
// URL is required; Title is passed through when the provider returns one.
type WebSearchResult struct {
	URL   string
	Title string
}

// SearXNGWebSearchProvider implements WebSearchProvider using SearXNG's JSON
// search endpoint.
type SearXNGWebSearchProvider struct {
	BaseURL string
	Client  *http.Client
}

type searXNGSearchResponse struct {
	Results []searXNGSearchResult `json:"results"`
}

type searXNGSearchResult struct {
	URL   string `json:"url"`
	Title string `json:"title"`
}

var defaultWebSearchClient = &http.Client{Timeout: webReadTimeout}

func NewSearXNGWebSearchProvider(baseURL string, client *http.Client) (*SearXNGWebSearchProvider, error) {
	normalizedBaseURL, err := normalizeSearXNGBaseURL(baseURL)
	if err != nil {
		return nil, err
	}
	return &SearXNGWebSearchProvider{BaseURL: normalizedBaseURL, Client: client}, nil
}

func (provider *SearXNGWebSearchProvider) Search(ctx context.Context, query string, limit int) ([]WebSearchResult, error) {
	if provider == nil {
		return nil, fmt.Errorf("SearXNG web search provider is not configured")
	}
	query = strings.TrimSpace(query)
	if query == "" {
		return nil, fmt.Errorf("web search query is required")
	}
	requestURL, err := provider.searchURL(query)
	if err != nil {
		return nil, err
	}
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
	if err != nil {
		return nil, err
	}
	response, err := provider.httpClient().Do(request)
	if err != nil {
		return nil, err
	}
	defer response.Body.Close()
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return nil, fmt.Errorf("SearXNG web search returned status %d", response.StatusCode)
	}
	var payload searXNGSearchResponse
	if err := json.NewDecoder(io.LimitReader(response.Body, maxWebReadBodyBytes+1)).Decode(&payload); err != nil {
		return nil, fmt.Errorf("decode SearXNG web search response: %w", err)
	}
	return normalizeSearXNGSearchResults(payload.Results, limit), nil
}

func (provider *SearXNGWebSearchProvider) httpClient() *http.Client {
	if provider != nil && provider.Client != nil {
		return provider.Client
	}
	return defaultWebSearchClient
}

func (provider *SearXNGWebSearchProvider) searchURL(query string) (string, error) {
	base, err := url.Parse(provider.BaseURL)
	if err != nil {
		return "", err
	}
	endpoint := *base
	endpoint.Path = strings.TrimRight(endpoint.Path, "/") + "/search"
	values := endpoint.Query()
	values.Set("q", query)
	values.Set("format", "json")
	endpoint.RawQuery = values.Encode()
	return endpoint.String(), nil
}

func normalizeSearXNGBaseURL(raw string) (string, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return "", fmt.Errorf("SearXNG base URL is required")
	}
	parsed, err := url.Parse(raw)
	if err != nil || !parsed.IsAbs() || parsed.Host == "" {
		return "", fmt.Errorf("SearXNG base URL is invalid")
	}
	if parsed.Scheme != "http" && parsed.Scheme != "https" {
		return "", fmt.Errorf("SearXNG base URL scheme is unsupported")
	}
	parsed.RawQuery = ""
	parsed.Fragment = ""
	return strings.TrimRight(parsed.String(), "/"), nil
}

func normalizeSearXNGSearchResults(items []searXNGSearchResult, limit int) []WebSearchResult {
	if limit <= 0 || limit > maxWebSearchResultURLs {
		limit = maxWebSearchResultURLs
	}
	seen := make(map[string]struct{}, min(limit, len(items)))
	results := make([]WebSearchResult, 0, min(limit, len(items)))
	for _, item := range items {
		if len(results) >= limit {
			break
		}
		itemURL, err := normalizePublicWebURL(item.URL)
		if err != nil {
			continue
		}
		if _, duplicate := seen[itemURL]; duplicate {
			continue
		}
		seen[itemURL] = struct{}{}
		results = append(results, WebSearchResult{
			URL:   itemURL,
			Title: strings.TrimSpace(item.Title),
		})
	}
	return results
}
