package arxiv

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

const (
	apiEndpoint        = "https://export.arxiv.org/api/query"
	maxFeedBytes       = 2 << 20
	totalHTTPTimeout   = 30 * time.Second
	connectHTTPTimeout = 5 * time.Second
	arxivUserAgent     = "agentteam-arxiv-extension/1.0"
)

type httpDoer interface {
	Do(*http.Request) (*http.Response, error)
}

func defaultHTTPClient() *http.Client {
	transport := http.DefaultTransport.(*http.Transport).Clone()
	transport.Proxy = nil
	transport.DialContext = (&net.Dialer{
		Timeout:   connectHTTPTimeout,
		KeepAlive: 30 * time.Second,
	}).DialContext
	return &http.Client{
		Transport: transport,
		Timeout:   totalHTTPTimeout,
		CheckRedirect: func(*http.Request, []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
}

func buildQueryURL(endpoint string, request SearchRequest) (string, error) {
	parsed, err := url.Parse(endpoint)
	if err != nil {
		return "", fmt.Errorf("parse arXiv endpoint: %w", err)
	}
	query := parsed.Query()
	query.Set("search_query", request.Query)
	query.Set("id_list", strings.Join(request.IDList, ","))
	query.Set("start", strconv.Itoa(request.Offset))
	query.Set("max_results", strconv.Itoa(request.Limit))
	query.Set("sortBy", request.SortBy)
	query.Set("sortOrder", request.SortOrder)
	parsed.RawQuery = query.Encode()
	return parsed.String(), nil
}

func fetchFeed(
	ctx context.Context,
	client httpDoer,
	endpoint string,
	request SearchRequest,
) (SearchResult, error) {
	requestURL, err := buildQueryURL(endpoint, request)
	if err != nil {
		return SearchResult{}, serviceError(
			ErrorUpstreamFailure,
			"arXiv request could not be created",
			err,
		)
	}
	httpRequest, err := http.NewRequestWithContext(
		ctx,
		http.MethodGet,
		requestURL,
		nil,
	)
	if err != nil {
		return SearchResult{}, serviceError(
			ErrorUpstreamFailure,
			"arXiv request could not be created",
			err,
		)
	}
	httpRequest.Header.Set("User-Agent", arxivUserAgent)
	response, err := client.Do(httpRequest)
	if err != nil {
		if errors.Is(err, context.Canceled) ||
			errors.Is(err, context.DeadlineExceeded) {
			return SearchResult{}, err
		}
		return SearchResult{}, serviceError(
			ErrorUpstreamFailure,
			"arXiv request failed",
			err,
		)
	}
	defer response.Body.Close()
	switch response.StatusCode {
	case http.StatusOK:
	case http.StatusBadRequest:
		return SearchResult{}, serviceError(
			ErrorValidation,
			"arXiv rejected the query",
			nil,
		)
	case http.StatusTooManyRequests:
		return SearchResult{}, serviceError(
			ErrorRateLimited,
			"arXiv rate limited the request",
			nil,
		)
	default:
		return SearchResult{}, serviceError(
			ErrorUpstreamFailure,
			"arXiv request failed",
			fmt.Errorf("unexpected HTTP status %d", response.StatusCode),
		)
	}
	body, err := readBounded(response.Body, maxFeedBytes)
	if err != nil {
		if errors.Is(err, context.Canceled) ||
			errors.Is(err, context.DeadlineExceeded) {
			return SearchResult{}, err
		}
		return SearchResult{}, serviceError(
			ErrorUpstreamFailure,
			"arXiv response was invalid",
			err,
		)
	}
	result, err := parseFeed(body)
	if err != nil {
		return SearchResult{}, serviceError(
			ErrorUpstreamFailure,
			"arXiv response was invalid",
			err,
		)
	}
	return result, nil
}

func readBounded(reader io.Reader, maximum int64) ([]byte, error) {
	body, err := io.ReadAll(io.LimitReader(reader, maximum+1))
	if err != nil {
		return nil, err
	}
	if int64(len(body)) > maximum {
		return nil, fmt.Errorf("response exceeded %d bytes", maximum)
	}
	return body, nil
}
