package arxiv

import (
	"context"
	"fmt"
	"regexp"
	"strings"
	"unicode/utf8"
)

const (
	SortByRelevance       = "relevance"
	SortByLastUpdatedDate = "lastUpdatedDate"
	SortBySubmittedDate   = "submittedDate"
	SortOrderAscending    = "ascending"
	SortOrderDescending   = "descending"
)

var (
	modernPaperID = regexp.MustCompile(`^[0-9]{4}\.[0-9]{4,5}(?:v[1-9][0-9]*)?$`)
	legacyPaperID = regexp.MustCompile(`^[A-Za-z0-9.-]+/[0-9]{7}(?:v[1-9][0-9]*)?$`)
	versionSuffix = regexp.MustCompile(`v[1-9][0-9]*$`)
)

type service struct {
	client   httpDoer
	endpoint string
}

func newService() *service {
	return &service{
		client:   defaultHTTPClient(),
		endpoint: apiEndpoint,
	}
}

func (service *service) Search(
	ctx context.Context,
	request SearchRequest,
) (SearchResult, error) {
	normalized, err := normalizeSearchRequest(request)
	if err != nil {
		return SearchResult{}, err
	}
	if service == nil || service.client == nil || service.endpoint == "" {
		return SearchResult{}, serviceError(
			ErrorUpstreamFailure,
			"arXiv service is unavailable",
			nil,
		)
	}
	result, err := fetchFeed(ctx, service.client, service.endpoint, normalized)
	if err != nil {
		return SearchResult{}, err
	}
	if result.Offset != normalized.Offset {
		return SearchResult{}, serviceError(
			ErrorUpstreamFailure,
			"arXiv response was invalid",
			fmt.Errorf(
				"start index %d did not match request %d",
				result.Offset,
				normalized.Offset,
			),
		)
	}
	if result.consumed > normalized.Limit {
		return SearchResult{}, serviceError(
			ErrorUpstreamFailure,
			"arXiv response exceeded the requested page size",
			fmt.Errorf(
				"received %d entries for limit %d",
				result.consumed,
				normalized.Limit,
			),
		)
	}
	if result.consumed == 0 && result.TotalResults > normalized.Offset {
		return SearchResult{}, serviceError(
			ErrorUpstreamFailure,
			"arXiv response was unexpectedly empty",
			nil,
		)
	}
	next := normalized.Offset + result.consumed
	resultWindowEnd := result.TotalResults
	if resultWindowEnd > MaxOffset {
		resultWindowEnd = MaxOffset
	}
	result.HasMore = result.consumed > 0 && next < resultWindowEnd
	if result.HasMore {
		result.NextOffset = &next
	}
	return result, nil
}

func (service *service) Get(
	ctx context.Context,
	request GetRequest,
) (PaperResult, error) {
	id := strings.TrimSpace(request.ID)
	if !validPaperID(id) {
		return PaperResult{}, serviceError(
			ErrorValidation,
			"arXiv paper id was invalid",
			nil,
		)
	}
	if service == nil || service.client == nil || service.endpoint == "" {
		return PaperResult{}, serviceError(
			ErrorUpstreamFailure,
			"arXiv service is unavailable",
			nil,
		)
	}
	result, err := fetchFeed(ctx, service.client, service.endpoint, SearchRequest{
		IDList:    []string{id},
		Limit:     1,
		SortBy:    SortByRelevance,
		SortOrder: SortOrderDescending,
	})
	if err != nil {
		return PaperResult{}, err
	}
	if len(result.Papers) == 0 {
		if result.TotalResults > 0 {
			return PaperResult{}, serviceError(
				ErrorUpstreamFailure,
				"arXiv response did not contain a valid paper",
				nil,
			)
		}
		return PaperResult{}, serviceError(
			ErrorNotFound,
			"arXiv paper was not found",
			nil,
		)
	}
	if len(result.Papers) != 1 || !paperIDMatchesRequest(result.Papers[0].ID, id) {
		return PaperResult{}, serviceError(
			ErrorUpstreamFailure,
			"arXiv response did not match the requested paper",
			nil,
		)
	}
	return PaperResult{Paper: result.Papers[0]}, nil
}

func normalizeSearchRequest(request SearchRequest) (SearchRequest, error) {
	trimmedQuery := strings.TrimSpace(request.Query)
	if utf8.RuneCountInString(request.Query) > 2048 ||
		(trimmedQuery == "" && len(request.IDList) == 0) {
		return SearchRequest{}, serviceError(
			ErrorValidation,
			"arXiv query was invalid",
			nil,
		)
	}
	if trimmedQuery == "" {
		request.Query = ""
	}
	if len(request.IDList) > 100 {
		return SearchRequest{}, serviceError(
			ErrorValidation,
			"arXiv id list was invalid",
			nil,
		)
	}
	request.IDList = append([]string(nil), request.IDList...)
	for index, id := range request.IDList {
		request.IDList[index] = strings.TrimSpace(id)
		if !validPaperID(request.IDList[index]) {
			return SearchRequest{}, serviceError(
				ErrorValidation,
				"arXiv id list was invalid",
				nil,
			)
		}
	}
	if request.Offset < 0 {
		return SearchRequest{}, serviceError(
			ErrorValidation,
			"arXiv offset was invalid",
			nil,
		)
	}
	if request.Limit == 0 {
		request.Limit = DefaultLimit
	}
	if request.Limit < 1 ||
		request.Limit > MaxLimit ||
		request.Offset+request.Limit > MaxOffset {
		return SearchRequest{}, serviceError(
			ErrorValidation,
			"arXiv result window was invalid",
			nil,
		)
	}
	if request.SortBy == "" {
		request.SortBy = SortByRelevance
	}
	switch request.SortBy {
	case SortByRelevance, SortByLastUpdatedDate, SortBySubmittedDate:
	default:
		return SearchRequest{}, serviceError(
			ErrorValidation,
			"arXiv sort criterion was invalid",
			nil,
		)
	}
	if request.SortOrder == "" {
		request.SortOrder = SortOrderDescending
	}
	switch request.SortOrder {
	case SortOrderAscending, SortOrderDescending:
	default:
		return SearchRequest{}, serviceError(
			ErrorValidation,
			"arXiv sort order was invalid",
			nil,
		)
	}
	return request, nil
}

func validPaperID(id string) bool {
	if modernPaperID.MatchString(id) {
		return true
	}
	return legacyPaperID.MatchString(id)
}

func paperIDMatchesRequest(actual, requested string) bool {
	if versionSuffix.MatchString(requested) {
		return actual == requested
	}
	return versionSuffix.ReplaceAllString(actual, "") == requested
}
