package runtime

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
)

func (service *Service) searchProvider(
	ctx context.Context,
	request SearchRequest,
	connection RuntimeConnection,
) ([]Result, error) {
	switch request.Provider {
	case ProviderSearXNG:
		return service.searchSearXNG(ctx, request, connection)
	case ProviderBrave:
		return service.searchBrave(ctx, request, connection)
	case ProviderTavily:
		return service.searchTavily(ctx, request, connection)
	case ProviderSerper:
		return service.searchSerper(ctx, request, connection)
	default:
		return nil, searchError(ErrorValidation, "search provider is invalid", nil)
	}
}

func (service *Service) searchSearXNG(
	ctx context.Context,
	search SearchRequest,
	connection RuntimeConnection,
) ([]Result, error) {
	base, err := parseSearXNGBaseURL(connection.BaseURL)
	if err != nil {
		return nil, searchError(
			ErrorProviderAuthUnavailable,
			"search provider connection is unavailable",
			err,
		)
	}
	endpoint := *base
	endpoint.Path = strings.TrimRight(endpoint.Path, "/") + "/search"
	query := endpoint.Query()
	query.Set("q", search.Query)
	query.Set("format", "json")
	if language := strings.TrimSpace(connection.Language); language != "" {
		query.Set("language", language)
	}
	endpoint.RawQuery = query.Encode()
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
	if err != nil {
		return nil, searchError(ErrorUpstreamFailure, "search provider request failed", err)
	}
	request.Header.Set("Accept", "application/json")

	var response struct {
		Results json.RawMessage `json:"results"`
	}
	if err := service.executeJSON(request, &response); err != nil {
		return nil, err
	}
	items, err := decodeProviderResultArray(response.Results, "results")
	if err != nil {
		return nil, err
	}
	results := make([]Result, 0, len(items))
	for _, encoded := range items {
		var item struct {
			URL     string `json:"url"`
			Title   string `json:"title"`
			Content string `json:"content"`
		}
		if err := json.Unmarshal(encoded, &item); err != nil {
			continue
		}
		results = append(results, Result{
			URL:     item.URL,
			Title:   item.Title,
			Snippet: item.Content,
		})
	}
	return results, nil
}

func (service *Service) searchBrave(
	ctx context.Context,
	search SearchRequest,
	connection RuntimeConnection,
) ([]Result, error) {
	apiKey, err := requireAPIKey(connection)
	if err != nil {
		return nil, err
	}
	endpoint, err := url.Parse(service.endpoints[ProviderBrave])
	if err != nil || !endpoint.IsAbs() {
		return nil, searchError(ErrorUpstreamFailure, "search provider is unavailable", err)
	}
	query := endpoint.Query()
	query.Set("q", search.Query)
	query.Set("count", strconv.Itoa(search.Limit))
	endpoint.RawQuery = query.Encode()
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
	if err != nil {
		return nil, searchError(ErrorUpstreamFailure, "search provider request failed", err)
	}
	request.Header.Set("Accept", "application/json")
	request.Header.Set("X-Subscription-Token", apiKey)

	var response struct {
		Web json.RawMessage `json:"web"`
	}
	if err := service.executeJSON(request, &response); err != nil {
		return nil, err
	}
	var web struct {
		Results json.RawMessage `json:"results"`
	}
	if err := decodeProviderObject(response.Web, "web", &web); err != nil {
		return nil, err
	}
	items, err := decodeProviderResultArray(web.Results, "web.results")
	if err != nil {
		return nil, err
	}
	results := make([]Result, 0, len(items))
	for _, encoded := range items {
		var item struct {
			URL         string `json:"url"`
			Title       string `json:"title"`
			Description string `json:"description"`
		}
		if err := json.Unmarshal(encoded, &item); err != nil {
			continue
		}
		results = append(results, Result{
			URL:     item.URL,
			Title:   item.Title,
			Snippet: item.Description,
		})
	}
	return results, nil
}

func (service *Service) searchTavily(
	ctx context.Context,
	search SearchRequest,
	connection RuntimeConnection,
) ([]Result, error) {
	apiKey, err := requireAPIKey(connection)
	if err != nil {
		return nil, err
	}
	body, err := json.Marshal(map[string]any{
		"query":       search.Query,
		"max_results": search.Limit,
	})
	if err != nil {
		return nil, searchError(ErrorUpstreamFailure, "search provider request failed", err)
	}
	request, err := http.NewRequestWithContext(
		ctx,
		http.MethodPost,
		service.endpoints[ProviderTavily],
		bytes.NewReader(body),
	)
	if err != nil {
		return nil, searchError(ErrorUpstreamFailure, "search provider request failed", err)
	}
	request.Header.Set("Accept", "application/json")
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("Authorization", "Bearer "+apiKey)

	var response struct {
		Results json.RawMessage `json:"results"`
	}
	if err := service.executeJSON(request, &response); err != nil {
		return nil, err
	}
	items, err := decodeProviderResultArray(response.Results, "results")
	if err != nil {
		return nil, err
	}
	results := make([]Result, 0, len(items))
	for _, encoded := range items {
		var item struct {
			URL     string `json:"url"`
			Title   string `json:"title"`
			Content string `json:"content"`
		}
		if err := json.Unmarshal(encoded, &item); err != nil {
			continue
		}
		results = append(results, Result{
			URL:     item.URL,
			Title:   item.Title,
			Snippet: item.Content,
		})
	}
	return results, nil
}

func (service *Service) searchSerper(
	ctx context.Context,
	search SearchRequest,
	connection RuntimeConnection,
) ([]Result, error) {
	apiKey, err := requireAPIKey(connection)
	if err != nil {
		return nil, err
	}
	body, err := json.Marshal(map[string]any{
		"q":   search.Query,
		"num": search.Limit,
	})
	if err != nil {
		return nil, searchError(ErrorUpstreamFailure, "search provider request failed", err)
	}
	request, err := http.NewRequestWithContext(
		ctx,
		http.MethodPost,
		service.endpoints[ProviderSerper],
		bytes.NewReader(body),
	)
	if err != nil {
		return nil, searchError(ErrorUpstreamFailure, "search provider request failed", err)
	}
	request.Header.Set("Accept", "application/json")
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("X-API-KEY", apiKey)

	var response struct {
		Organic json.RawMessage `json:"organic"`
	}
	if err := service.executeJSON(request, &response); err != nil {
		return nil, err
	}
	items, err := decodeProviderResultArray(response.Organic, "organic")
	if err != nil {
		return nil, err
	}
	positioned := make([]positionedResult, 0, len(items))
	for _, encoded := range items {
		var item struct {
			Position int    `json:"position"`
			Link     string `json:"link"`
			Title    string `json:"title"`
			Snippet  string `json:"snippet"`
		}
		if err := json.Unmarshal(encoded, &item); err != nil || item.Position <= 0 {
			continue
		}
		positioned = append(positioned, positionedResult{
			position: item.Position,
			result: Result{
				URL:     item.Link,
				Title:   item.Title,
				Snippet: item.Snippet,
			},
		})
	}
	sort.SliceStable(positioned, func(left int, right int) bool {
		return positioned[left].position < positioned[right].position
	})
	results := make([]Result, 0, len(positioned))
	for _, item := range positioned {
		results = append(results, item.result)
	}
	return results, nil
}

type positionedResult struct {
	position int
	result   Result
}

func (service *Service) executeJSON(request *http.Request, destination any) error {
	response, err := service.client.Do(request)
	if err != nil {
		return searchError(
			ErrorUpstreamFailure,
			"search provider request failed",
			internalError("execute provider request", err),
		)
	}
	defer response.Body.Close()
	switch response.StatusCode {
	case http.StatusUnauthorized, http.StatusForbidden:
		return searchError(
			ErrorProviderAuthUnavailable,
			"search provider credential was rejected",
			fmt.Errorf("provider returned %d", response.StatusCode),
		)
	case http.StatusTooManyRequests:
		return searchError(
			ErrorRateLimited,
			"search provider rate limited the request",
			fmt.Errorf("provider returned %d", response.StatusCode),
		)
	}
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return searchError(
			ErrorUpstreamFailure,
			"search provider rejected the request",
			fmt.Errorf("provider returned %d", response.StatusCode),
		)
	}
	payload, err := io.ReadAll(io.LimitReader(response.Body, MaxRawResponseBytes+1))
	if err != nil {
		return searchError(
			ErrorUpstreamFailure,
			"search provider response failed",
			internalError("read provider response", err),
		)
	}
	if len(payload) > MaxRawResponseBytes {
		return searchError(
			ErrorUpstreamFailure,
			"search provider response exceeded the size limit",
			nil,
		)
	}
	decoder := json.NewDecoder(bytes.NewReader(payload))
	if err := decoder.Decode(destination); err != nil {
		return searchError(
			ErrorUpstreamFailure,
			"search provider response was invalid",
			internalError("decode provider response", err),
		)
	}
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		return searchError(
			ErrorUpstreamFailure,
			"search provider response was invalid",
			errorsForTrailingJSON(err),
		)
	}
	return nil
}

func errorsForTrailingJSON(err error) error {
	if err == nil {
		return fmt.Errorf("provider response contained multiple JSON values")
	}
	return fmt.Errorf("decode trailing provider response: %w", err)
}

func decodeProviderObject(
	raw json.RawMessage,
	field string,
	destination any,
) error {
	if missingProviderField(raw) {
		return invalidProviderResponse(
			field,
			errors.New("required provider object is missing"),
		)
	}
	if err := json.Unmarshal(raw, destination); err != nil {
		return invalidProviderResponse(field, err)
	}
	return nil
}

func decodeProviderResultArray(
	raw json.RawMessage,
	field string,
) ([]json.RawMessage, error) {
	if missingProviderField(raw) {
		return nil, invalidProviderResponse(
			field,
			errors.New("required provider result array is missing"),
		)
	}
	var items []json.RawMessage
	if err := json.Unmarshal(raw, &items); err != nil || items == nil {
		if err == nil {
			err = errors.New("provider result array is null")
		}
		return nil, invalidProviderResponse(field, err)
	}
	return items, nil
}

func missingProviderField(raw json.RawMessage) bool {
	trimmed := bytes.TrimSpace(raw)
	return len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null"))
}

func invalidProviderResponse(field string, cause error) error {
	return searchError(
		ErrorUpstreamFailure,
		"search provider response was invalid",
		fmt.Errorf("decode provider response field %s: %w", field, cause),
	)
}

func requireAPIKey(connection RuntimeConnection) (string, error) {
	apiKey := strings.TrimSpace(connection.APIKey)
	if apiKey == "" || strings.ContainsAny(apiKey, "\r\n") {
		return "", searchError(
			ErrorProviderAuthUnavailable,
			"search provider credential is unavailable",
			nil,
		)
	}
	return apiKey, nil
}

// ValidateSearXNGBaseURL validates the administrator-owned connection origin.
func ValidateSearXNGBaseURL(raw string) error {
	_, err := parseSearXNGBaseURL(raw)
	return err
}

func parseSearXNGBaseURL(raw string) (*url.URL, error) {
	parsed, err := url.Parse(strings.TrimSpace(raw))
	if err != nil ||
		!parsed.IsAbs() ||
		parsed.Host == "" ||
		(parsed.Scheme != "http" && parsed.Scheme != "https") ||
		parsed.User != nil ||
		parsed.RawQuery != "" ||
		parsed.Fragment != "" {
		return nil, fmt.Errorf("SearXNG base URL is invalid")
	}
	return parsed, nil
}
