package httpapi

import (
	"errors"
	"fmt"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"
	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/codersdk"
)

// QueryParamParser is a helper for parsing all query params and gathering all
// errors in 1 sweep. This means all invalid fields are returned at once,
// rather than only returning the first error
type QueryParamParser struct {
	// Errors is the set of errors to return via the API. If the length
	// of this set is 0, there are no errors!.
	Errors []codersdk.ValidationError
	// Parsed is a map of all query params that were parsed. This is useful
	// for checking if extra query params were passed in.
	Parsed map[string]bool
	// RequiredNotEmptyParams is a map of all query params that are required. This is useful
	// for forcing a value to be provided.
	RequiredNotEmptyParams map[string]bool
}

func NewQueryParamParser() *QueryParamParser {
	return &QueryParamParser{
		Errors:                 []codersdk.ValidationError{},
		Parsed:                 map[string]bool{},
		RequiredNotEmptyParams: map[string]bool{},
	}
}

// ErrorExcessParams checks if any query params were passed in that were not
// parsed. If so, it adds an error to the parser as these values are not valid
// query parameters.
func (p *QueryParamParser) ErrorExcessParams(values url.Values) {
	for k := range values {
		if _, ok := p.Parsed[k]; !ok {
			p.Errors = append(p.Errors, codersdk.ValidationError{
				Field:  k,
				Detail: fmt.Sprintf("%q is not a valid query param", k),
			})
		}
	}
}

func (p *QueryParamParser) addParsed(key string) {
	p.Parsed[key] = true
}

func (p *QueryParamParser) UInt(vals url.Values, def uint64, queryParam string) uint64 {
	v, err := parseQueryParam(p, vals, func(v string) (uint64, error) {
		return strconv.ParseUint(v, 10, 64)
	}, def, queryParam)
	if err != nil {
		p.Errors = append(p.Errors, codersdk.ValidationError{
			Field:  queryParam,
			Detail: fmt.Sprintf("Query param %q must be a valid positive integer: %s", queryParam, err.Error()),
		})
		return 0
	}
	return v
}

func (p *QueryParamParser) Int(vals url.Values, def int, queryParam string) int {
	v, err := parseQueryParam(p, vals, strconv.Atoi, def, queryParam)
	if err != nil {
		p.Errors = append(p.Errors, codersdk.ValidationError{
			Field:  queryParam,
			Detail: fmt.Sprintf("Query param %q must be a valid integer: %s", queryParam, err.Error()),
		})
	}
	return v
}

// PositiveInt32 function checks if the given value is 32-bit and positive.
//
// We can't use `uint32` as the value must be within the range  <0,2147483647>
// as database expects it. Otherwise, the database query fails with `pq: OFFSET must not be negative`.
func (p *QueryParamParser) PositiveInt32(vals url.Values, def int32, queryParam string) int32 {
	v, err := parseQueryParam(p, vals, func(v string) (int32, error) {
		intValue, err := strconv.ParseInt(v, 10, 32)
		if err != nil {
			return 0, err
		}
		if intValue < 0 {
			return 0, xerrors.Errorf("value is negative")
		}
		return int32(intValue), nil
	}, def, queryParam)
	if err != nil {
		p.Errors = append(p.Errors, codersdk.ValidationError{
			Field:  queryParam,
			Detail: fmt.Sprintf("Query param %q must be a valid 32-bit positive integer: %s", queryParam, err.Error()),
		})
	}
	return v
}

func (p *QueryParamParser) Boolean(vals url.Values, def bool, queryParam string) bool {
	v, err := parseQueryParam(p, vals, strconv.ParseBool, def, queryParam)
	if err != nil {
		p.Errors = append(p.Errors, codersdk.ValidationError{
			Field:  queryParam,
			Detail: fmt.Sprintf("Query param %q must be a valid boolean: %s", queryParam, err.Error()),
		})
	}
	return v
}

func (p *QueryParamParser) RequiredNotEmpty(queryParam ...string) *QueryParamParser {
	for _, q := range queryParam {
		p.RequiredNotEmptyParams[q] = true
	}
	return p
}

func (p *QueryParamParser) UUIDorMe(vals url.Values, def uuid.UUID, me uuid.UUID, queryParam string) uuid.UUID {
	return ParseCustom(p, vals, def, queryParam, func(v string) (uuid.UUID, error) {
		if v == "me" {
			return me, nil
		}
		return uuid.Parse(v)
	})
}

func (p *QueryParamParser) UUID(vals url.Values, def uuid.UUID, queryParam string) uuid.UUID {
	v, err := parseQueryParam(p, vals, uuid.Parse, def, queryParam)
	if err != nil {
		p.Errors = append(p.Errors, codersdk.ValidationError{
			Field:  queryParam,
			Detail: fmt.Sprintf("Query param %q must be a valid uuid", queryParam),
		})
	}
	return v
}

func (p *QueryParamParser) UUIDs(vals url.Values, def []uuid.UUID, queryParam string) []uuid.UUID {
	return ParseCustomList(p, vals, def, queryParam, func(v string) (uuid.UUID, error) {
		return uuid.Parse(strings.TrimSpace(v))
	})
}

func (p *QueryParamParser) RedirectURL(vals url.Values, base *url.URL, queryParam string) *url.URL {
	v, err := parseQueryParam(p, vals, url.Parse, base, queryParam)
	if err != nil {
		p.Errors = append(p.Errors, codersdk.ValidationError{
			Field:  queryParam,
			Detail: fmt.Sprintf("Query param %q must be a valid url: %s", queryParam, err.Error()),
		})
	}

	// It can be a sub-directory but not a sub-domain, as we have apps on
	// sub-domains and that seems too dangerous.
	if v.Host != base.Host || !strings.HasPrefix(v.Path, base.Path) {
		p.Errors = append(p.Errors, codersdk.ValidationError{
			Field:  queryParam,
			Detail: fmt.Sprintf("Query param %q must be a subset of %s", queryParam, base),
		})
	}

	return v
}

func (p *QueryParamParser) Time(vals url.Values, def time.Time, queryParam, layout string) time.Time {
	return p.timeWithMutate(vals, def, queryParam, layout, nil)
}

// Time uses the default time format of RFC3339Nano and always returns a UTC time.
func (p *QueryParamParser) Time3339Nano(vals url.Values, def time.Time, queryParam string) time.Time {
	layout := time.RFC3339Nano
	return p.timeWithMutate(vals, def, queryParam, layout, func(term string) string {
		// All search queries are forced to lowercase. But the RFC format requires
		// upper case letters. So just uppercase the term.
		return strings.ToUpper(term)
	})
}

func (p *QueryParamParser) timeWithMutate(vals url.Values, def time.Time, queryParam, layout string, mutate func(term string) string) time.Time {
	v, err := parseQueryParam(p, vals, func(term string) (time.Time, error) {
		if mutate != nil {
			term = mutate(term)
		}
		t, err := time.Parse(layout, term)
		if err != nil {
			return time.Time{}, err
		}
		return t.UTC(), nil
	}, def, queryParam)
	if err != nil {
		p.Errors = append(p.Errors, codersdk.ValidationError{
			Field:  queryParam,
			Detail: fmt.Sprintf("Query param %q must be a valid date format (%s): %s", queryParam, layout, err.Error()),
		})
	}
	return v
}

func (p *QueryParamParser) String(vals url.Values, def string, queryParam string) string {
	v, err := parseQueryParam(p, vals, func(v string) (string, error) {
		return v, nil
	}, def, queryParam)
	if err != nil {
		p.Errors = append(p.Errors, codersdk.ValidationError{
			Field:  queryParam,
			Detail: fmt.Sprintf("Query param %q must be a valid string: %s", queryParam, err.Error()),
		})
	}
	return v
}

func (p *QueryParamParser) Strings(vals url.Values, def []string, queryParam string) []string {
	return ParseCustomList(p, vals, def, queryParam, func(v string) (string, error) {
		return v, nil
	})
}

// ValidEnum represents an enum that can be parsed and validated.
type ValidEnum interface {
	// Add more types as needed (avoid importing large dependency trees).
	~string

	// Valid is required on the enum type to be used with ParseEnum.
	Valid() bool
}

// ParseEnum is a function that can be passed into ParseCustom that handles enum
// validation.
func ParseEnum[T ValidEnum](term string) (T, error) {
	enum := T(term)
	if enum.Valid() {
		return enum, nil
	}
	var empty T
	return empty, xerrors.Errorf("%q is not a valid value", term)
}

// ParseCustom has to be a function, not a method on QueryParamParser because generics
// cannot be used on struct methods.
func ParseCustom[T any](parser *QueryParamParser, vals url.Values, def T, queryParam string, parseFunc func(v string) (T, error)) T {
	v, err := parseQueryParam(parser, vals, parseFunc, def, queryParam)
	if err != nil {
		parser.Errors = append(parser.Errors, codersdk.ValidationError{
			Field:  queryParam,
			Detail: fmt.Sprintf("Query param %q has invalid value: %s", queryParam, err.Error()),
		})
	}
	return v
}

// ParseCustomList is a function that handles csv query params or multiple values
// for a query param.
// Csv is supported as it is a common way to pass multiple values in a query param.
// Multiple values is supported (key=value&key=value2) for feature parity with GitHub issue search.
func ParseCustomList[T any](parser *QueryParamParser, vals url.Values, def []T, queryParam string, parseFunc func(v string) (T, error)) []T {
	v, err := parseQueryParamSet(parser, vals, func(set []string) ([]T, error) {
		// Gather all terms.
		allTerms := make([]string, 0, len(set))
		for _, s := range set {
			// If a term is a csv, break it out into individual terms.
			terms := strings.Split(s, ",")
			allTerms = append(allTerms, terms...)
		}

		var badErrors error
		var output []T
		for _, s := range allTerms {
			good, err := parseFunc(s)
			if err != nil {
				badErrors = errors.Join(badErrors, err)
				continue
			}
			output = append(output, good)
		}
		if badErrors != nil {
			return []T{}, badErrors
		}

		return output, nil
	}, def, queryParam)
	if err != nil {
		parser.Errors = append(parser.Errors, codersdk.ValidationError{
			Field:  queryParam,
			Detail: fmt.Sprintf("Query param %q has invalid values: %s", queryParam, err.Error()),
		})
	}
	return v
}

// parseQueryParam expects just 1 value set for the given query param.
func parseQueryParam[T any](parser *QueryParamParser, vals url.Values, parse func(v string) (T, error), def T, queryParam string) (T, error) {
	setParse := func(set []string) (T, error) {
		if len(set) > 1 {
			// Set as a parser.Error rather than return an error.
			// Returned errors are errors from the passed in `parse` function, and
			// imply the query param value had attempted to be parsed.
			// By raising the error this way, we can also more easily control how it
			// is presented to the user. A returned error is wrapped with more text.
			parser.Errors = append(parser.Errors, codersdk.ValidationError{
				Field:  queryParam,
				Detail: fmt.Sprintf("Query param %q provided more than once, found %d times. Only provide 1 instance of this query param.", queryParam, len(set)),
			})
			return def, nil
		}
		return parse(set[0])
	}
	return parseQueryParamSet(parser, vals, setParse, def, queryParam)
}

func parseQueryParamSet[T any](parser *QueryParamParser, vals url.Values, parse func(set []string) (T, error), def T, queryParam string) (T, error) {
	parser.addParsed(queryParam)
	// If the query param is required and not present, return an error.
	if parser.RequiredNotEmptyParams[queryParam] && (!vals.Has(queryParam) || vals.Get(queryParam) == "") {
		parser.Errors = append(parser.Errors, codersdk.ValidationError{
			Field:  queryParam,
			Detail: fmt.Sprintf("Query param %q is required and cannot be empty", queryParam),
		})
		return def, nil
	}

	// If the query param is not present, return the default value.
	if !vals.Has(queryParam) || vals.Get(queryParam) == "" {
		return def, nil
	}

	return parse(vals[queryParam])
}
