//  Copyright (c) 2014 Couchbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 		http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package bleve

import (
	"bytes"
	"encoding/json"
	"fmt"
	"reflect"
	"regexp"
	"slices"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/blevesearch/bleve/v2/analysis"
	"github.com/blevesearch/bleve/v2/analysis/datetime/optional"
	"github.com/blevesearch/bleve/v2/document"
	"github.com/blevesearch/bleve/v2/registry"
	"github.com/blevesearch/bleve/v2/search"
	"github.com/blevesearch/bleve/v2/search/collector"
	"github.com/blevesearch/bleve/v2/search/query"
	"github.com/blevesearch/bleve/v2/size"
	"github.com/blevesearch/bleve/v2/util"
)

var (
	reflectStaticSizeSearchResult int
	reflectStaticSizeSearchStatus int
)

func init() {
	var sr SearchResult
	reflectStaticSizeSearchResult = int(reflect.TypeOf(sr).Size())
	var ss SearchStatus
	reflectStaticSizeSearchStatus = int(reflect.TypeOf(ss).Size())
}

var cache = registry.NewCache()

const defaultDateTimeParser = optional.Name

const (
	ScoreDefault = ""
	ScoreNone    = "none"
	ScoreRRF     = "rrf"
	ScoreRSF     = "rsf"
)

var AllowedFusionSort = search.SortOrder{&search.SortScore{Desc: true}}

type dateTimeRange struct {
	Name           string    `json:"name,omitempty"`
	Start          time.Time `json:"start,omitempty"`
	End            time.Time `json:"end,omitempty"`
	DateTimeParser string    `json:"datetime_parser,omitempty"`
	startString    *string
	endString      *string
}

func (dr *dateTimeRange) ParseDates(dateTimeParser analysis.DateTimeParser) (start, end time.Time, err error) {
	start = dr.Start
	if dr.Start.IsZero() && dr.startString != nil {
		s, _, parseError := dateTimeParser.ParseDateTime(*dr.startString)
		if parseError != nil {
			return start, end, fmt.Errorf("error parsing start date '%s' for date range name '%s': %v", *dr.startString, dr.Name, parseError)
		}
		start = s
	}
	end = dr.End
	if dr.End.IsZero() && dr.endString != nil {
		e, _, parseError := dateTimeParser.ParseDateTime(*dr.endString)
		if parseError != nil {
			return start, end, fmt.Errorf("error parsing end date '%s' for date range name '%s': %v", *dr.endString, dr.Name, parseError)
		}
		end = e
	}
	return start, end, err
}

func (dr *dateTimeRange) UnmarshalJSON(input []byte) error {
	var temp struct {
		Name           string  `json:"name,omitempty"`
		Start          *string `json:"start,omitempty"`
		End            *string `json:"end,omitempty"`
		DateTimeParser string  `json:"datetime_parser,omitempty"`
	}

	if err := util.UnmarshalJSON(input, &temp); err != nil {
		return err
	}

	dr.Name = temp.Name
	if temp.Start != nil {
		dr.startString = temp.Start
	}
	if temp.End != nil {
		dr.endString = temp.End
	}
	if temp.DateTimeParser != "" {
		dr.DateTimeParser = temp.DateTimeParser
	}

	return nil
}

func (dr *dateTimeRange) MarshalJSON() ([]byte, error) {
	rv := map[string]interface{}{
		"name": dr.Name,
	}

	if !dr.Start.IsZero() {
		rv["start"] = dr.Start
	} else if dr.startString != nil {
		rv["start"] = dr.startString
	}

	if !dr.End.IsZero() {
		rv["end"] = dr.End
	} else if dr.endString != nil {
		rv["end"] = dr.endString
	}

	if dr.DateTimeParser != "" {
		rv["datetime_parser"] = dr.DateTimeParser
	}
	return util.MarshalJSON(rv)
}

type numericRange struct {
	Name string   `json:"name,omitempty"`
	Min  *float64 `json:"min,omitempty"`
	Max  *float64 `json:"max,omitempty"`
}

// A FacetRequest describes a facet or aggregation
// of the result document set you would like to be
// built.
type FacetRequest struct {
	Size           int              `json:"size"`
	Field          string           `json:"field"`
	TermPrefix     string           `json:"term_prefix,omitempty"`
	TermPattern    string           `json:"term_pattern,omitempty"`
	NumericRanges  []*numericRange  `json:"numeric_ranges,omitempty"`
	DateTimeRanges []*dateTimeRange `json:"date_ranges,omitempty"`

	// Compiled regex pattern (cached during validation)
	compiledPattern *regexp.Regexp
}

// NewFacetRequest creates a facet on the specified
// field that limits the number of entries to the
// specified size.
func NewFacetRequest(field string, size int) *FacetRequest {
	return &FacetRequest{
		Field: field,
		Size:  size,
	}
}

// SetPrefixFilter sets the prefix filter for term facets.
func (fr *FacetRequest) SetPrefixFilter(prefix string) {
	fr.TermPrefix = prefix
}

// SetRegexFilter sets the regex pattern filter for term facets.
func (fr *FacetRequest) SetRegexFilter(pattern string) {
	fr.TermPattern = pattern
}

func (fr *FacetRequest) Validate() error {
	// Validate regex pattern if provided and cache the compiled regex
	if fr.TermPattern != "" {
		compiled, err := regexp.Compile(fr.TermPattern)
		if err != nil {
			return fmt.Errorf("invalid term pattern: %v", err)
		}
		fr.compiledPattern = compiled
	}

	nrCount := len(fr.NumericRanges)
	drCount := len(fr.DateTimeRanges)
	if nrCount > 0 && drCount > 0 {
		return fmt.Errorf("facet can only contain numeric ranges or date ranges, not both")
	}

	if nrCount > 0 {
		nrNames := map[string]interface{}{}
		for _, nr := range fr.NumericRanges {
			if _, ok := nrNames[nr.Name]; ok {
				return fmt.Errorf("numeric ranges contains duplicate name '%s'", nr.Name)
			}
			nrNames[nr.Name] = struct{}{}
			if nr.Min == nil && nr.Max == nil {
				return fmt.Errorf("numeric range query must specify either min, max or both for range name '%s'", nr.Name)
			}
		}

	} else {
		dateTimeParser, err := cache.DateTimeParserNamed(defaultDateTimeParser)
		if err != nil {
			return err
		}
		drNames := map[string]interface{}{}
		for _, dr := range fr.DateTimeRanges {
			if _, ok := drNames[dr.Name]; ok {
				return fmt.Errorf("date ranges contains duplicate name '%s'", dr.Name)
			}
			drNames[dr.Name] = struct{}{}
			if dr.DateTimeParser == "" {
				// cannot parse the date range dates as the defaultDateTimeParser is overridden
				// so perform this validation at query time
				start, end, err := dr.ParseDates(dateTimeParser)
				if err != nil {
					return fmt.Errorf("ParseDates err: %v, using date time parser named %s", err, defaultDateTimeParser)
				}
				if start.IsZero() && end.IsZero() {
					return fmt.Errorf("date range query must specify either start, end or both for range name '%s'", dr.Name)
				}
			}
		}
	}

	return nil
}

// AddDateTimeRange adds a bucket to a field
// containing date values.  Documents with a
// date value falling into this range are tabulated
// as part of this bucket/range.
func (fr *FacetRequest) AddDateTimeRange(name string, start, end time.Time) {
	if fr.DateTimeRanges == nil {
		fr.DateTimeRanges = make([]*dateTimeRange, 0, 1)
	}
	fr.DateTimeRanges = append(fr.DateTimeRanges, &dateTimeRange{Name: name, Start: start, End: end})
}

// AddDateTimeRangeString adds a bucket to a field
// containing date values. Uses defaultDateTimeParser to parse the date strings.
func (fr *FacetRequest) AddDateTimeRangeString(name string, start, end *string) {
	if fr.DateTimeRanges == nil {
		fr.DateTimeRanges = make([]*dateTimeRange, 0, 1)
	}
	fr.DateTimeRanges = append(fr.DateTimeRanges,
		&dateTimeRange{Name: name, startString: start, endString: end})
}

// AddDateTimeRangeString adds a bucket to a field
// containing date values. Uses the specified parser to parse the date strings.
// provided the parser is registered in the index mapping.
func (fr *FacetRequest) AddDateTimeRangeStringWithParser(name string, start, end *string, parser string) {
	if fr.DateTimeRanges == nil {
		fr.DateTimeRanges = make([]*dateTimeRange, 0, 1)
	}
	fr.DateTimeRanges = append(fr.DateTimeRanges,
		&dateTimeRange{Name: name, startString: start, endString: end, DateTimeParser: parser})
}

// AddNumericRange adds a bucket to a field
// containing numeric values.  Documents with a
// numeric value falling into this range are
// tabulated as part of this bucket/range.
func (fr *FacetRequest) AddNumericRange(name string, min, max *float64) {
	if fr.NumericRanges == nil {
		fr.NumericRanges = make([]*numericRange, 0, 1)
	}
	fr.NumericRanges = append(fr.NumericRanges, &numericRange{Name: name, Min: min, Max: max})
}

// FacetsRequest groups together all the
// FacetRequest objects for a single query.
type FacetsRequest map[string]*FacetRequest

func (fr FacetsRequest) Validate() error {
	for _, v := range fr {
		if err := v.Validate(); err != nil {
			return err
		}
	}
	return nil
}

// HighlightRequest describes how field matches
// should be highlighted.
type HighlightRequest struct {
	Style  *string  `json:"style"`
	Fields []string `json:"fields"`
}

// NewHighlight creates a default
// HighlightRequest.
func NewHighlight() *HighlightRequest {
	return &HighlightRequest{}
}

// NewHighlightWithStyle creates a HighlightRequest
// with an alternate style.
func NewHighlightWithStyle(style string) *HighlightRequest {
	return &HighlightRequest{
		Style: &style,
	}
}

func (h *HighlightRequest) AddField(field string) {
	if h.Fields == nil {
		h.Fields = make([]string, 0, 1)
	}
	h.Fields = append(h.Fields, field)
}

func (r *SearchRequest) Validate() error {
	if srq, ok := r.Query.(query.ValidatableQuery); ok {
		err := srq.Validate()
		if err != nil {
			return err
		}
	}

	if r.SearchAfter != nil && r.SearchBefore != nil {
		return fmt.Errorf("cannot use search after and search before together")
	}

	if r.SearchAfter != nil {
		if r.From != 0 {
			return fmt.Errorf("cannot use search after with from !=0")
		}
		if len(r.SearchAfter) != len(r.Sort) {
			return fmt.Errorf("search after must have same size as sort order")
		}
	}
	if r.SearchBefore != nil {
		if r.From != 0 {
			return fmt.Errorf("cannot use search before with from !=0")
		}
		if len(r.SearchBefore) != len(r.Sort) {
			return fmt.Errorf("search before must have same size as sort order")
		}
	}

	err := r.validatePagination()
	if err != nil {
		return err
	}

	if IsScoreFusionRequested(r) {
		if r.SearchAfter != nil || r.SearchBefore != nil {
			return fmt.Errorf("cannot use search after or search before with score fusion")
		}

		if r.Sort != nil {
			if !reflect.DeepEqual(r.Sort, AllowedFusionSort) {
				return fmt.Errorf("sort must be empty or descending order of score for score fusion")
			}
		}
	}

	err = validateKNN(r)
	if err != nil {
		return err
	}
	return r.Facets.Validate()
}

// Validates SearchAfter/SearchBefore
func (r *SearchRequest) validatePagination() error {
	var pagination []string
	var afterOrBefore string

	if r.SearchAfter != nil {
		pagination = r.SearchAfter
		afterOrBefore = "search after"
	} else if r.SearchBefore != nil {
		pagination = r.SearchBefore
		afterOrBefore = "search before"
	} else {
		return nil
	}

	for i := range pagination {
		switch ss := r.Sort[i].(type) {
		case *search.SortGeoDistance:
			_, err := strconv.ParseFloat(pagination[i], 64)
			if err != nil {
				return fmt.Errorf("invalid %s value for sort field '%s': '%s'. %s", afterOrBefore, ss.Field, pagination[i], err)
			}
		case *search.SortField:
			switch ss.Type {
			case search.SortFieldAsNumber:
				_, err := strconv.ParseFloat(pagination[i], 64)
				if err != nil {
					return fmt.Errorf("invalid %s value for sort field '%s': '%s'. %s", afterOrBefore, ss.Field, pagination[i], err)
				}
			case search.SortFieldAsDate:
				_, err := time.Parse(time.RFC3339Nano, pagination[i])
				if err != nil {
					return fmt.Errorf("invalid %s value for sort field '%s': '%s'. %s", afterOrBefore, ss.Field, pagination[i], err)
				}
			}
		}
	}

	return nil
}

// AddFacet adds a FacetRequest to this SearchRequest
func (r *SearchRequest) AddFacet(facetName string, f *FacetRequest) {
	if r.Facets == nil {
		r.Facets = make(FacetsRequest, 1)
	}
	r.Facets[facetName] = f
}

// SortBy changes the request to use the requested sort order
// this form uses the simplified syntax with an array of strings
// each string can either be a field name
// or the magic value _id and _score which refer to the doc id and search score
// any of these values can optionally be prefixed with - to reverse the order
func (r *SearchRequest) SortBy(order []string) {
	so := search.ParseSortOrderStrings(order)
	r.Sort = so
}

// SortByCustom changes the request to use the requested sort order
func (r *SearchRequest) SortByCustom(order search.SortOrder) {
	r.Sort = order
}

// SetSearchAfter sets the request to skip over hits with a sort
// value less than the provided sort after key
func (r *SearchRequest) SetSearchAfter(after []string) {
	r.SearchAfter = after
}

// SetSearchBefore sets the request to skip over hits with a sort
// value greater than the provided sort before key
func (r *SearchRequest) SetSearchBefore(before []string) {
	r.SearchBefore = before
}

// AddParams adds a RequestParams field to the search request
func (r *SearchRequest) AddParams(params RequestParams) {
	r.Params = &params
}

// NewSearchRequest creates a new SearchRequest
// for the Query, using default values for all
// other search parameters.
func NewSearchRequest(q query.Query) *SearchRequest {
	return NewSearchRequestOptions(q, 10, 0, false)
}

// NewSearchRequestOptions creates a new SearchRequest
// for the Query, with the requested size, from
// and explanation search parameters.
// By default results are ordered by score, descending.
func NewSearchRequestOptions(q query.Query, size, from int, explain bool) *SearchRequest {
	return &SearchRequest{
		Query:   q,
		Size:    size,
		From:    from,
		Explain: explain,
		Sort:    search.SortOrder{&search.SortScore{Desc: true}},
	}
}

// IndexErrMap tracks errors with the name of the index where it occurred
type IndexErrMap map[string]error

// MarshalJSON serializes the error into a string for JSON consumption
func (iem IndexErrMap) MarshalJSON() ([]byte, error) {
	tmp := make(map[string]string, len(iem))
	for k, v := range iem {
		tmp[k] = v.Error()
	}
	return util.MarshalJSON(tmp)
}

func (iem IndexErrMap) UnmarshalJSON(data []byte) error {
	var tmp map[string]string
	err := util.UnmarshalJSON(data, &tmp)
	if err != nil {
		return err
	}
	for k, v := range tmp {
		iem[k] = fmt.Errorf("%s", v)
	}
	return nil
}

// SearchStatus is a section in the SearchResult reporting how many
// underlying indexes were queried, how many were successful/failed
// and a map of any errors that were encountered
type SearchStatus struct {
	Total      int         `json:"total"`
	Failed     int         `json:"failed"`
	Successful int         `json:"successful"`
	Errors     IndexErrMap `json:"errors,omitempty"`
}

// Merge will merge together multiple SearchStatuses during a MultiSearch
func (ss *SearchStatus) Merge(other *SearchStatus) {
	ss.Total += other.Total
	ss.Failed += other.Failed
	ss.Successful += other.Successful
	if len(other.Errors) > 0 {
		if ss.Errors == nil {
			ss.Errors = make(map[string]error)
		}
		for otherIndex, otherError := range other.Errors {
			ss.Errors[otherIndex] = otherError
		}
	}
}

// A SearchResult describes the results of executing
// a SearchRequest.
//
// Status - Whether the search was executed on the underlying indexes successfully
// or failed, and the corresponding errors.
// Request - The SearchRequest that was executed.
// Hits - The list of documents that matched the query and their corresponding
// scores, score explanation, location info and so on.
// Total - The total number of documents that matched the query.
// Cost - indicates how expensive was the query with respect to bytes read
// from the mapped index files.
// MaxScore - The maximum score seen across all document hits seen for this query.
// Took - The time taken to execute the search.
// Facets - The facet results for the search.
type SearchResult struct {
	Status   *SearchStatus                  `json:"status"`
	Request  *SearchRequest                 `json:"request,omitempty"`
	Hits     search.DocumentMatchCollection `json:"hits"`
	Total    uint64                         `json:"total_hits"`
	Cost     uint64                         `json:"cost"`
	MaxScore float64                        `json:"max_score"`
	Took     time.Duration                  `json:"took"`
	Facets   search.FacetResults            `json:"facets"`
	// special fields that are applicable only for search
	// results that are obtained from a presearch
	SynonymResult search.FieldTermSynonymMap `json:"synonym_result,omitempty"`

	// The following fields are applicable to BM25 preSearch
	BM25Stats *search.BM25Stats `json:"bm25_stats,omitempty"`
}

func (sr *SearchResult) Size() int {
	sizeInBytes := reflectStaticSizeSearchResult + size.SizeOfPtr +
		reflectStaticSizeSearchStatus

	for _, entry := range sr.Hits {
		if entry != nil {
			sizeInBytes += entry.Size()
		}
	}

	for k, v := range sr.Facets {
		sizeInBytes += size.SizeOfString + len(k) +
			v.Size()
	}

	return sizeInBytes
}

func (sr *SearchResult) String() string {
	rv := &strings.Builder{}
	if sr.Total > 0 {
		switch {
		case sr.Request != nil && sr.Request.Size > 0:
			start := sr.Request.From + 1
			end := sr.Request.From + len(sr.Hits)
			fmt.Fprintf(rv, "%d matches, showing %d through %d, took %s\n", sr.Total, start, end, sr.Took)
			for i, hit := range sr.Hits {
				rv = formatHit(rv, hit, start+i)
			}
		case sr.Request == nil:
			fmt.Fprintf(rv, "%d matches, took %s\n", sr.Total, sr.Took)
			for i, hit := range sr.Hits {
				rv = formatHit(rv, hit, i+1)
			}
		default:
			fmt.Fprintf(rv, "%d matches, took %s\n", sr.Total, sr.Took)
		}
	} else {
		fmt.Fprintf(rv, "No matches\n")
	}
	if len(sr.Facets) > 0 {
		fmt.Fprintf(rv, "Facets:\n")
		for fn, f := range sr.Facets {
			fmt.Fprintf(rv, "%s(%d)\n", fn, f.Total)
			for _, t := range f.Terms.Terms() {
				fmt.Fprintf(rv, "\t%s(%d)\n", t.Term, t.Count)
			}
			for _, n := range f.NumericRanges {
				fmt.Fprintf(rv, "\t%s(%d)\n", n.Name, n.Count)
			}
			for _, d := range f.DateRanges {
				fmt.Fprintf(rv, "\t%s(%d)\n", d.Name, d.Count)
			}
			if f.Other != 0 {
				fmt.Fprintf(rv, "\tOther(%d)\n", f.Other)
			}
		}
	}
	return rv.String()
}

// formatHit is a helper function to format a single hit in the search result for
// the String() method of SearchResult
func formatHit(rv *strings.Builder, hit *search.DocumentMatch, hitNumber int) *strings.Builder {
	fmt.Fprintf(rv, "%5d. %s (%f)\n", hitNumber, hit.ID, hit.Score)
	for fragmentField, fragments := range hit.Fragments {
		fmt.Fprintf(rv, "\t%s\n", fragmentField)
		for _, fragment := range fragments {
			fmt.Fprintf(rv, "\t\t%s\n", fragment)
		}
	}
	for otherFieldName, otherFieldValue := range hit.Fields {
		if otherFieldName == NestedDocumentKey {
			continue
		}
		if _, ok := hit.Fragments[otherFieldName]; !ok {
			fmt.Fprintf(rv, "\t%s\n", otherFieldName)
			fmt.Fprintf(rv, "\t\t%v\n", otherFieldValue)
		}
	}
	// nested documents
	if nested, ok := hit.Fields[NestedDocumentKey]; ok {
		if list, ok := nested.([]*search.NestedDocumentMatch); ok {
			fmt.Fprintf(rv, "\t%s (%d nested documents)\n", NestedDocumentKey, len(list))
			for ni, nd := range list {
				fmt.Fprintf(rv, "\t\tNested #%d:\n", ni+1)
				for f, frags := range nd.Fragments {
					fmt.Fprintf(rv, "\t\t\t%s\n", f)
					for _, frag := range frags {
						fmt.Fprintf(rv, "\t\t\t\t%s\n", frag)
					}
				}
				for f, v := range nd.Fields {
					if _, ok := nd.Fragments[f]; !ok {
						fmt.Fprintf(rv, "\t\t\t%s\n", f)
						fmt.Fprintf(rv, "\t\t\t\t%v\n", v)
					}
				}
			}
		}
	}
	if len(hit.DecodedSort) > 0 {
		fmt.Fprintf(rv, "\t_sort: [")
		for k, v := range hit.DecodedSort {
			if k > 0 {
				fmt.Fprintf(rv, ", ")
			}
			fmt.Fprintf(rv, "%v", v)
		}
		fmt.Fprintf(rv, "]\n")
	}
	return rv
}

// Merge will merge together multiple SearchResults during a MultiSearch
func (sr *SearchResult) Merge(other *SearchResult) {
	sr.Status.Merge(other.Status)
	sr.Hits = append(sr.Hits, other.Hits...)
	sr.Total += other.Total
	sr.Cost += other.Cost
	if other.MaxScore > sr.MaxScore {
		sr.MaxScore = other.MaxScore
	}
	if sr.Facets == nil && len(other.Facets) != 0 {
		sr.Facets = other.Facets
		return
	}

	sr.Facets.Merge(other.Facets)
}

// MemoryNeededForSearchResult is an exported helper function to determine the RAM
// needed to accommodate the results for a given search request.
func MemoryNeededForSearchResult(req *SearchRequest) uint64 {
	if req == nil {
		return 0
	}

	numDocMatches := req.Size + req.From
	if req.Size+req.From > collector.PreAllocSizeSkipCap {
		numDocMatches = collector.PreAllocSizeSkipCap
	}

	estimate := 0

	// overhead from the SearchResult structure
	var sr SearchResult
	estimate += sr.Size()

	var dm search.DocumentMatch
	sizeOfDocumentMatch := dm.Size()

	// overhead from results
	estimate += numDocMatches * sizeOfDocumentMatch

	// overhead from facet results
	if req.Facets != nil {
		var fr search.FacetResult
		estimate += len(req.Facets) * fr.Size()
	}

	// overhead from fields, highlighting
	var d document.Document
	if len(req.Fields) > 0 || req.Highlight != nil {
		numDocsApplicable := req.Size
		if numDocsApplicable > collector.PreAllocSizeSkipCap {
			numDocsApplicable = collector.PreAllocSizeSkipCap
		}
		estimate += numDocsApplicable * d.Size()
	}

	return uint64(estimate)
}

// SetSortFunc sets the sort implementation to use when sorting hits.
//
// SearchRequests can specify a custom sort implementation to meet
// their needs. For instance, by specifying a parallel sort
// that uses all available cores.
func (r *SearchRequest) SetSortFunc(s func(sort.Interface)) {
	r.sortFunc = s
}

// SortFunc returns the sort implementation to use when sorting hits.
// Defaults to sort.Sort.
func (r *SearchRequest) SortFunc() func(data sort.Interface) {
	if r.sortFunc != nil {
		return r.sortFunc
	}

	return sort.Sort
}

func isMatchNoneQuery(q query.Query) bool {
	_, ok := q.(*query.MatchNoneQuery)
	return ok
}

func isMatchAllQuery(q query.Query) bool {
	_, ok := q.(*query.MatchAllQuery)
	return ok
}

// Checks if the request is hybrid search. Currently supports: RRF, RSF.
func IsScoreFusionRequested(req *SearchRequest) bool {
	switch req.Score {
	case ScoreRRF, ScoreRSF:
		return true
	default:
		return false
	}
}

// Additional parameters in the search request. Currently only being
// used for score fusion parameters.
type RequestParams struct {
	ScoreRankConstant int `json:"score_rank_constant,omitempty"`
	ScoreWindowSize   int `json:"score_window_size,omitempty"`
}

func NewDefaultParams(from, size int) *RequestParams {
	return &RequestParams{
		ScoreRankConstant: DefaultScoreRankConstant,
		ScoreWindowSize:   from + size,
	}
}

func (p *RequestParams) UnmarshalJSON(input []byte) error {
	var temp struct {
		ScoreRankConstant *int `json:"score_rank_constant,omitempty"`
		ScoreWindowSize   *int `json:"score_window_size,omitempty"`
	}

	if err := util.UnmarshalJSON(input, &temp); err != nil {
		return err
	}

	if temp.ScoreRankConstant != nil {
		p.ScoreRankConstant = *temp.ScoreRankConstant
	}

	if temp.ScoreWindowSize != nil {
		p.ScoreWindowSize = *temp.ScoreWindowSize
	}

	return nil
}

func (p *RequestParams) Validate(size int) error {
	if p.ScoreWindowSize < 1 {
		return fmt.Errorf("score window size must be greater than 0")
	} else if p.ScoreWindowSize < size {
		return fmt.Errorf("score window size must be greater than or equal to Size (%d)", size)
	}

	return nil
}

func ParseParams(r *SearchRequest, input []byte) (*RequestParams, error) {
	params := NewDefaultParams(r.From, r.Size)
	if len(input) == 0 {
		return params, nil
	}

	err := util.UnmarshalJSON(input, params)
	if err != nil {
		return nil, err
	}

	// validate params
	err = params.Validate(r.Size)
	if err != nil {
		return nil, err
	}

	return params, nil
}

// OptionalRawMessage is a wrapper around json.RawMessage that treats empty or `null` JSON as nil.
type OptionalRawMessage json.RawMessage

func (n *OptionalRawMessage) UnmarshalJSON(data []byte) error {
	if len(data) == 0 || bytes.Equal(data, []byte("null")) {
		*n = nil
		return nil
	}
	*n = slices.Clone(data)
	return nil
}

func (n OptionalRawMessage) MarshalJSON() ([]byte, error) {
	if len(n) == 0 {
		return []byte("null"), nil
	}
	return n, nil
}
