// Package runtime owns provider-neutral execution for search engine
// Builtin Extensions and their bounded provider adapters.
package runtime

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strings"
)

var defaultEndpoints = map[string]string{
	ProviderBrave:  "https://api.search.brave.com/res/v1/web/search",
	ProviderTavily: "https://api.tavily.com/search",
	ProviderSerper: "https://google.serper.dev/search",
}

// Service resolves App connections and executes provider-specific searches.
type Service struct {
	connections ConnectionReader
	client      *http.Client
	endpoints   map[string]string
}

// New constructs a normalized search service.
func New(connections ConnectionReader) *Service {
	endpoints := make(map[string]string, len(defaultEndpoints))
	for provider, endpoint := range defaultEndpoints {
		endpoints[provider] = endpoint
	}
	return &Service{
		connections: connections,
		client:      defaultHTTPClient(),
		endpoints:   endpoints,
	}
}

// Search executes one fixed-provider search and returns normalized results.
func (service *Service) Search(
	ctx context.Context,
	request SearchRequest,
) (SearchResult, error) {
	request.Query = strings.TrimSpace(request.Query)
	if err := validateRequest(request); err != nil {
		return SearchResult{}, err
	}
	if request.Limit == 0 {
		request.Limit = DefaultLimit
	}
	if service == nil || service.connections == nil || service.client == nil {
		return SearchResult{}, searchError(
			ErrorUpstreamFailure,
			"search service is unavailable",
			nil,
		)
	}

	connection, err := service.connections.ResolveSearchEngineRuntimeConnection(
		ctx,
		request.WorkspaceID,
		request.ServiceInstallationID,
		request.Provider,
	)
	if err != nil {
		if errors.Is(err, context.Canceled) ||
			errors.Is(err, context.DeadlineExceeded) {
			return SearchResult{}, err
		}
		if CodeOf(err) != "" {
			return SearchResult{}, err
		}
		if errors.Is(err, ErrRuntimeConnectionUnavailable) {
			return SearchResult{}, searchError(
				ErrorProviderAuthUnavailable,
				"search provider connection is unavailable",
				err,
			)
		}
		return SearchResult{}, searchError(
			ErrorUpstreamFailure,
			"search provider connection lookup failed",
			err,
		)
	}
	if connection.Provider != request.Provider {
		return SearchResult{}, searchError(
			ErrorProviderAuthUnavailable,
			"search provider connection is unavailable",
			fmt.Errorf("connection provider %q does not match %q", connection.Provider, request.Provider),
		)
	}

	results, err := service.searchProvider(ctx, request, connection)
	if err != nil {
		return SearchResult{}, err
	}
	return SearchResult{Results: normalizeResults(results, request.Limit)}, nil
}

// TestConnection performs one bounded provider request without returning
// search results or requiring an Agent invocation principal.
func (service *Service) TestConnection(
	ctx context.Context,
	connection RuntimeConnection,
) error {
	if service == nil || service.client == nil {
		return searchError(ErrorUpstreamFailure, "search service is unavailable", nil)
	}
	switch connection.Provider {
	case ProviderSearXNG, ProviderBrave, ProviderTavily, ProviderSerper:
	default:
		return searchError(ErrorValidation, "search provider is invalid", nil)
	}
	_, err := service.searchProvider(ctx, SearchRequest{
		Provider: connection.Provider,
		Query:    "AgentTeam",
		Limit:    1,
	}, connection)
	return err
}

func validateRequest(request SearchRequest) error {
	switch request.Provider {
	case ProviderSearXNG, ProviderBrave, ProviderTavily, ProviderSerper:
	default:
		return searchError(ErrorValidation, "search provider is invalid", nil)
	}
	if strings.TrimSpace(request.WorkspaceID) == "" ||
		strings.TrimSpace(request.AgentID) == "" ||
		strings.TrimSpace(request.AgentMemberID) == "" ||
		strings.TrimSpace(request.ServiceInstallationID) == "" {
		return searchError(ErrorValidation, "search invocation principal is invalid", nil)
	}
	if request.Query == "" || len([]rune(request.Query)) > maxQueryRunes {
		return searchError(ErrorValidation, "search query is invalid", nil)
	}
	if request.Limit < 0 || request.Limit > MaxLimit {
		return searchError(ErrorValidation, "search limit is invalid", nil)
	}
	return nil
}

func normalizeResults(input []Result, limit int) []Result {
	if limit <= 0 || limit > MaxLimit {
		limit = DefaultLimit
	}
	results := make([]Result, 0, min(limit, len(input)))
	seen := make(map[string]struct{}, min(limit, len(input)))
	for _, candidate := range input {
		if len(results) >= limit {
			break
		}
		parsed, err := url.Parse(strings.TrimSpace(candidate.URL))
		if err != nil ||
			(parsed.Scheme != "http" && parsed.Scheme != "https") ||
			parsed.Host == "" ||
			parsed.User != nil {
			continue
		}
		parsed.Fragment = ""
		normalizedURL := parsed.String()
		if _, duplicate := seen[normalizedURL]; duplicate {
			continue
		}
		seen[normalizedURL] = struct{}{}
		results = append(results, Result{
			URL:     normalizedURL,
			Title:   truncateRunes(strings.TrimSpace(candidate.Title), maxTitleRunes),
			Snippet: truncateRunes(strings.TrimSpace(candidate.Snippet), maxSnippetRunes),
		})
	}
	return results
}

func truncateRunes(value string, limit int) string {
	runes := []rune(value)
	if len(runes) <= limit {
		return value
	}
	return string(runes[:limit])
}
