package structs

import (
	"encoding/json"
	"fmt"
	"math"
	"net/http"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/mitchellh/hashstructure"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/agent/cache"
	"github.com/hashicorp/consul/lib"
)

const (
	// Names of Envoy's LB policies
	LBPolicyMaglev       = "maglev"
	LBPolicyRingHash     = "ring_hash"
	LBPolicyRandom       = "random"
	LBPolicyLeastRequest = "least_request"
	LBPolicyRoundRobin   = "round_robin"

	// Names of Envoy's LB policies
	HashPolicyCookie     = "cookie"
	HashPolicyHeader     = "header"
	HashPolicyQueryParam = "query_parameter"
)

var (
	validLBPolicies = map[string]bool{
		"":                   true,
		LBPolicyRandom:       true,
		LBPolicyRoundRobin:   true,
		LBPolicyLeastRequest: true,
		LBPolicyRingHash:     true,
		LBPolicyMaglev:       true,
	}

	validHashPolicies = map[string]bool{
		HashPolicyHeader:     true,
		HashPolicyCookie:     true,
		HashPolicyQueryParam: true,
	}
)

// ServiceRouterConfigEntry defines L7 (e.g. http) routing rules for a named
// service exposed in Connect.
//
// This config entry represents the topmost part of the discovery chain. Only
// one router config will be used per resolved discovery chain and is not
// otherwise discovered recursively (unlike splitter and resolver config
// entries).
//
// Router config entries will be restricted to only services that define their
// protocol as http-based (in centralized configuration).
type ServiceRouterConfigEntry struct {
	Kind string
	Name string

	// Routes is the list of routes to consider when processing L7 requests.
	// The first rule to match in the list is terminal and stops further
	// evaluation.
	//
	// Traffic that fails to match any of the provided routes will be routed to
	// the default service.
	Routes []ServiceRoute

	Meta           map[string]string `json:",omitempty"`
	EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
	RaftIndex
}

func (e *ServiceRouterConfigEntry) GetKind() string {
	return ServiceRouter
}

func (e *ServiceRouterConfigEntry) GetName() string {
	if e == nil {
		return ""
	}

	return e.Name
}

func (e *ServiceRouterConfigEntry) GetMeta() map[string]string {
	if e == nil {
		return nil
	}
	return e.Meta
}

func (e *ServiceRouterConfigEntry) Normalize() error {
	if e == nil {
		return fmt.Errorf("config entry is nil")
	}

	e.Kind = ServiceRouter

	e.EnterpriseMeta.Normalize()

	for _, route := range e.Routes {
		if route.Match == nil || route.Match.HTTP == nil {
			continue
		}

		httpMatch := route.Match.HTTP
		for j := 0; j < len(httpMatch.Methods); j++ {
			httpMatch.Methods[j] = strings.ToUpper(httpMatch.Methods[j])
		}

		if route.Destination != nil && route.Destination.Namespace == "" {
			route.Destination.Namespace = e.EnterpriseMeta.NamespaceOrEmpty()
		}
	}

	return nil
}

func (e *ServiceRouterConfigEntry) Validate() error {
	if e.Name == "" {
		return fmt.Errorf("Name is required")
	}

	if err := validateConfigEntryMeta(e.Meta); err != nil {
		return err
	}

	// Technically you can have no explicit routes at all where just the
	// catch-all is configured for you, but at that point maybe you should just
	// delete it so it will default?

	for i, route := range e.Routes {
		eligibleForPrefixRewrite := false
		if route.Match != nil && route.Match.HTTP != nil {
			pathParts := 0
			if route.Match.HTTP.PathExact != "" {
				eligibleForPrefixRewrite = true
				pathParts++
				if !strings.HasPrefix(route.Match.HTTP.PathExact, "/") {
					return fmt.Errorf("Route[%d] PathExact doesn't start with '/': %q", i, route.Match.HTTP.PathExact)
				}
			}
			if route.Match.HTTP.PathPrefix != "" {
				eligibleForPrefixRewrite = true
				pathParts++
				if !strings.HasPrefix(route.Match.HTTP.PathPrefix, "/") {
					return fmt.Errorf("Route[%d] PathPrefix doesn't start with '/': %q", i, route.Match.HTTP.PathPrefix)
				}
			}
			if route.Match.HTTP.PathRegex != "" {
				pathParts++
			}
			if pathParts > 1 {
				return fmt.Errorf("Route[%d] should only contain at most one of PathExact, PathPrefix, or PathRegex", i)
			}

			for j, hdr := range route.Match.HTTP.Header {
				if hdr.Name == "" {
					return fmt.Errorf("Route[%d] Header[%d] missing required Name field", i, j)
				}
				hdrParts := 0
				if hdr.Present {
					hdrParts++
				}
				if hdr.Exact != "" {
					hdrParts++
				}
				if hdr.Regex != "" {
					hdrParts++
				}
				if hdr.Prefix != "" {
					hdrParts++
				}
				if hdr.Suffix != "" {
					hdrParts++
				}
				if hdrParts != 1 {
					return fmt.Errorf("Route[%d] Header[%d] should only contain one of Present, Exact, Prefix, Suffix, or Regex", i, j)
				}
			}

			for j, qm := range route.Match.HTTP.QueryParam {
				if qm.Name == "" {
					return fmt.Errorf("Route[%d] QueryParam[%d] missing required Name field", i, j)
				}

				qmParts := 0
				if qm.Present {
					qmParts++
				}
				if qm.Exact != "" {
					qmParts++
				}
				if qm.Regex != "" {
					qmParts++
				}
				if qmParts != 1 {
					return fmt.Errorf("Route[%d] QueryParam[%d] should only contain one of Present, Exact, or Regex", i, j)
				}
			}

			if len(route.Match.HTTP.Methods) > 0 {
				found := make(map[string]struct{})
				for _, m := range route.Match.HTTP.Methods {
					if !isValidHTTPMethod(m) {
						return fmt.Errorf("Route[%d] Methods contains an invalid method %q", i, m)
					}
					if _, ok := found[m]; ok {
						return fmt.Errorf("Route[%d] Methods contains %q more than once", i, m)
					}
					found[m] = struct{}{}
				}
			}
		}

		if route.Destination != nil {
			if route.Destination.PrefixRewrite != "" && !eligibleForPrefixRewrite {
				return fmt.Errorf("Route[%d] cannot make use of PrefixRewrite without configuring either PathExact or PathPrefix", i)
			}
		}
	}

	return nil
}

func isValidHTTPMethod(method string) bool {
	switch method {
	case http.MethodGet,
		http.MethodHead,
		http.MethodPost,
		http.MethodPut,
		http.MethodPatch,
		http.MethodDelete,
		http.MethodConnect,
		http.MethodOptions,
		http.MethodTrace:
		return true
	default:
		return false
	}
}

func (e *ServiceRouterConfigEntry) CanRead(rule acl.Authorizer) bool {
	return canReadDiscoveryChain(e, rule)
}

func (e *ServiceRouterConfigEntry) CanWrite(rule acl.Authorizer) bool {
	return canWriteDiscoveryChain(e, rule)
}

func (e *ServiceRouterConfigEntry) GetRaftIndex() *RaftIndex {
	if e == nil {
		return &RaftIndex{}
	}

	return &e.RaftIndex
}

func (e *ServiceRouterConfigEntry) ListRelatedServices() []ServiceID {
	found := make(map[ServiceID]struct{})

	// We always inject a default catch-all route to the same service as the router.
	svcID := NewServiceID(e.Name, &e.EnterpriseMeta)
	found[svcID] = struct{}{}

	for _, route := range e.Routes {
		if route.Destination != nil {
			destID := NewServiceID(defaultIfEmpty(route.Destination.Service, e.Name), route.Destination.GetEnterpriseMeta(&e.EnterpriseMeta))
			if destID != svcID {
				found[destID] = struct{}{}
			}
		}
	}

	if len(found) == 0 {
		return nil
	}

	out := make([]ServiceID, 0, len(found))
	for svc := range found {
		out = append(out, svc)
	}
	sort.Slice(out, func(i, j int) bool {
		return out[i].EnterpriseMeta.LessThan(&out[j].EnterpriseMeta) ||
			out[i].ID < out[j].ID
	})
	return out
}

func (e *ServiceRouterConfigEntry) GetEnterpriseMeta() *EnterpriseMeta {
	if e == nil {
		return nil
	}

	return &e.EnterpriseMeta
}

// ServiceRoute is a single routing rule that routes traffic to the destination
// when the match criteria applies.
type ServiceRoute struct {
	Match       *ServiceRouteMatch       `json:",omitempty"`
	Destination *ServiceRouteDestination `json:",omitempty"`
}

// ServiceRouteMatch is a set of criteria that can match incoming L7 requests.
type ServiceRouteMatch struct {
	HTTP *ServiceRouteHTTPMatch `json:",omitempty"`

	// If we have non-http match criteria for other protocols in the future
	// (gRPC, redis, etc) they can go here.
}

func (m *ServiceRouteMatch) IsEmpty() bool {
	return m.HTTP == nil || m.HTTP.IsEmpty()
}

// ServiceRouteHTTPMatch is a set of http-specific match criteria.
type ServiceRouteHTTPMatch struct {
	PathExact  string `json:",omitempty" alias:"path_exact"`
	PathPrefix string `json:",omitempty" alias:"path_prefix"`
	PathRegex  string `json:",omitempty" alias:"path_regex"`

	Header     []ServiceRouteHTTPMatchHeader     `json:",omitempty"`
	QueryParam []ServiceRouteHTTPMatchQueryParam `json:",omitempty" alias:"query_param"`
	Methods    []string                          `json:",omitempty"`
}

func (m *ServiceRouteHTTPMatch) IsEmpty() bool {
	return m.PathExact == "" &&
		m.PathPrefix == "" &&
		m.PathRegex == "" &&
		len(m.Header) == 0 &&
		len(m.QueryParam) == 0 &&
		len(m.Methods) == 0
}

type ServiceRouteHTTPMatchHeader struct {
	Name    string
	Present bool   `json:",omitempty"`
	Exact   string `json:",omitempty"`
	Prefix  string `json:",omitempty"`
	Suffix  string `json:",omitempty"`
	Regex   string `json:",omitempty"`
	Invert  bool   `json:",omitempty"`
}

type ServiceRouteHTTPMatchQueryParam struct {
	Name    string
	Present bool   `json:",omitempty"`
	Exact   string `json:",omitempty"`
	Regex   string `json:",omitempty"`
}

// ServiceRouteDestination describes how to proxy the actual matching request
// to a service.
type ServiceRouteDestination struct {
	// Service is the service to resolve instead of the default service. If
	// empty then the default discovery chain service name is used.
	Service string `json:",omitempty"`

	// ServiceSubset is a named subset of the given service to resolve instead
	// of one defined as that service's DefaultSubset. If empty the default
	// subset is used.
	//
	// If this field is specified then this route is ineligible for further
	// splitting.
	ServiceSubset string `json:",omitempty" alias:"service_subset"`

	// Namespace is the namespace to resolve the service from instead of the
	// current namespace. If empty the current namespace is assumed.
	//
	// If this field is specified then this route is ineligible for further
	// splitting.
	Namespace string `json:",omitempty"`

	// PrefixRewrite allows for the proxied request to have its matching path
	// prefix modified before being sent to the destination. Described more
	// below in the envoy implementation section.
	PrefixRewrite string `json:",omitempty" alias:"prefix_rewrite"`

	// RequestTimeout is the total amount of time permitted for the entire
	// downstream request (and retries) to be processed.
	RequestTimeout time.Duration `json:",omitempty" alias:"request_timeout"`

	// NumRetries is the number of times to retry the request when a retryable
	// result occurs. This seems fairly proxy agnostic.
	NumRetries uint32 `json:",omitempty" alias:"num_retries"`

	// RetryOnConnectFailure allows for connection failure errors to trigger a
	// retry. This should be expressible in other proxies as it's just a layer
	// 4 failure bubbling up to layer 7.
	RetryOnConnectFailure bool `json:",omitempty" alias:"retry_on_connect_failure"`

	// RetryOnStatusCodes is a flat list of http response status codes that are
	// eligible for retry. This again should be feasible in any sane proxy.
	RetryOnStatusCodes []uint32 `json:",omitempty" alias:"retry_on_status_codes"`
}

func (e *ServiceRouteDestination) MarshalJSON() ([]byte, error) {
	type Alias ServiceRouteDestination
	exported := &struct {
		RequestTimeout string `json:",omitempty"`
		*Alias
	}{
		RequestTimeout: e.RequestTimeout.String(),
		Alias:          (*Alias)(e),
	}
	if e.RequestTimeout == 0 {
		exported.RequestTimeout = ""
	}

	return json.Marshal(exported)
}

func (e *ServiceRouteDestination) UnmarshalJSON(data []byte) error {
	type Alias ServiceRouteDestination
	aux := &struct {
		RequestTimeout string
		*Alias
	}{
		Alias: (*Alias)(e),
	}
	if err := lib.UnmarshalJSON(data, &aux); err != nil {
		return err
	}
	var err error
	if aux.RequestTimeout != "" {
		if e.RequestTimeout, err = time.ParseDuration(aux.RequestTimeout); err != nil {
			return err
		}
	}
	return nil
}

func (d *ServiceRouteDestination) HasRetryFeatures() bool {
	return d.NumRetries > 0 || d.RetryOnConnectFailure || len(d.RetryOnStatusCodes) > 0
}

// ServiceSplitterConfigEntry defines how incoming requests are split across
// different subsets of a single service (like during staged canary rollouts),
// or perhaps across different services (like during a v2 rewrite or other type
// of codebase migration).
//
// This config entry represents the next hop of the discovery chain after
// routing. If no splitter config is defined the chain assumes 100% of traffic
// goes to the default service and discovery continues on to the resolution
// hop.
//
// Splitter configs are recursively collected while walking the discovery
// chain.
//
// Splitter config entries will be restricted to only services that define
// their protocol as http-based (in centralized configuration).
type ServiceSplitterConfigEntry struct {
	Kind string
	Name string

	// Splits is the configurations for the details of the traffic splitting.
	//
	// The sum of weights across all splits must add up to 100.
	//
	// If the split is within epsilon of 100 then the remainder is attributed
	// to the FIRST split.
	Splits []ServiceSplit

	Meta           map[string]string `json:",omitempty"`
	EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
	RaftIndex
}

func (e *ServiceSplitterConfigEntry) GetKind() string {
	return ServiceSplitter
}

func (e *ServiceSplitterConfigEntry) GetName() string {
	if e == nil {
		return ""
	}

	return e.Name
}

func (e *ServiceSplitterConfigEntry) GetMeta() map[string]string {
	if e == nil {
		return nil
	}
	return e.Meta
}

func (e *ServiceSplitterConfigEntry) Normalize() error {
	if e == nil {
		return fmt.Errorf("config entry is nil")
	}

	e.Kind = ServiceSplitter

	// This slightly massages inputs to enforce that the smallest representable
	// weight is 1/10000 or .01%

	e.EnterpriseMeta.Normalize()

	if len(e.Splits) > 0 {
		for i, split := range e.Splits {
			if split.Namespace == "" {
				split.Namespace = e.EnterpriseMeta.NamespaceOrDefault()
			}
			e.Splits[i].Weight = NormalizeServiceSplitWeight(split.Weight)
		}
	}

	return nil
}

func NormalizeServiceSplitWeight(weight float32) float32 {
	weightScaled := scaleWeight(weight)
	return float32(weightScaled) / 100.0
}

func (e *ServiceSplitterConfigEntry) Validate() error {
	if e.Name == "" {
		return fmt.Errorf("Name is required")
	}

	if len(e.Splits) == 0 {
		return fmt.Errorf("no splits configured")
	}

	if err := validateConfigEntryMeta(e.Meta); err != nil {
		return err
	}

	const maxScaledWeight = 100 * 100

	copyAsKey := func(s ServiceSplit) ServiceSplit {
		s.Weight = 0
		return s
	}

	// Make sure we didn't refer to the same thing twice.
	found := make(map[ServiceSplit]struct{})
	for _, split := range e.Splits {
		splitKey := copyAsKey(split)
		if splitKey.Service == "" {
			splitKey.Service = e.Name
		}
		if _, ok := found[splitKey]; ok {
			return fmt.Errorf(
				"split destination occurs more than once: service=%q, subset=%q, namespace=%q",
				splitKey.Service, splitKey.ServiceSubset, splitKey.Namespace,
			)
		}
		found[splitKey] = struct{}{}
	}

	sumScaled := 0
	for _, split := range e.Splits {
		sumScaled += scaleWeight(split.Weight)
	}

	if sumScaled != maxScaledWeight {
		return fmt.Errorf("the sum of all split weights must be 100, not %f", float32(sumScaled)/100)
	}

	return nil
}

// scaleWeight assumes the input is a value between 0 and 100 representing
// shares out of a percentile range. The function will convert to a unit
// representing 0.01% units in the same manner as you may convert $0.98 to 98
// cents.
func scaleWeight(v float32) int {
	return int(math.Round(float64(v * 100.0)))
}

func (e *ServiceSplitterConfigEntry) CanRead(rule acl.Authorizer) bool {
	return canReadDiscoveryChain(e, rule)
}

func (e *ServiceSplitterConfigEntry) CanWrite(rule acl.Authorizer) bool {
	return canWriteDiscoveryChain(e, rule)
}

func (e *ServiceSplitterConfigEntry) GetRaftIndex() *RaftIndex {
	if e == nil {
		return &RaftIndex{}
	}

	return &e.RaftIndex
}

func (e *ServiceSplitterConfigEntry) GetEnterpriseMeta() *EnterpriseMeta {
	if e == nil {
		return nil
	}

	return &e.EnterpriseMeta
}

func (e *ServiceSplitterConfigEntry) ListRelatedServices() []ServiceID {
	found := make(map[ServiceID]struct{})

	svcID := NewServiceID(e.Name, &e.EnterpriseMeta)
	for _, split := range e.Splits {
		splitID := NewServiceID(defaultIfEmpty(split.Service, e.Name), split.GetEnterpriseMeta(&e.EnterpriseMeta))

		if splitID != svcID {
			found[splitID] = struct{}{}
		}
	}

	if len(found) == 0 {
		return nil
	}

	out := make([]ServiceID, 0, len(found))
	for svc := range found {
		out = append(out, svc)
	}
	sort.Slice(out, func(i, j int) bool {
		return out[i].EnterpriseMeta.LessThan(&out[j].EnterpriseMeta) ||
			out[i].ID < out[j].ID
	})
	return out
}

// ServiceSplit defines how much traffic to send to which set of service
// instances during a traffic split.
type ServiceSplit struct {
	// A value between 0 and 100 reflecting what portion of traffic should be
	// directed to this split.
	//
	// The smallest representable weight is 1/10000 or .01%
	//
	// If the split is within epsilon of 100 then the remainder is attributed
	// to the FIRST split.
	Weight float32

	// Service is the service to resolve instead of the default (optional).
	Service string `json:",omitempty"`

	// ServiceSubset is a named subset of the given service to resolve instead
	// of one defined as that service's DefaultSubset. If empty the default
	// subset is used (optional).
	//
	// If this field is specified then this route is ineligible for further
	// splitting.
	ServiceSubset string `json:",omitempty" alias:"service_subset"`

	// Namespace is the namespace to resolve the service from instead of the
	// current namespace. If empty the current namespace is assumed (optional).
	//
	// If this field is specified then this route is ineligible for further
	// splitting.
	Namespace string `json:",omitempty"`
}

// ServiceResolverConfigEntry defines which instances of a service should
// satisfy discovery requests for a given named service.
//
// This config entry represents the next hop of the discovery chain after
// splitting. If no resolver config is defined the chain assumes 100% of
// traffic goes to the healthy instances of the default service in the current
// datacenter+namespace and discovery terminates.
//
// Resolver configs are recursively collected while walking the chain.
//
// Resolver config entries will be valid for services defined with any protocol
// (in centralized configuration).
type ServiceResolverConfigEntry struct {
	Kind string
	Name string

	// DefaultSubset is the subset to use when no explicit subset is
	// requested. If empty the unnamed subset is used.
	DefaultSubset string `json:",omitempty" alias:"default_subset"`

	// Subsets is a map of subset name to subset definition for all
	// usable named subsets of this service. The map key is the name
	// of the subset and all names must be valid DNS subdomain elements
	// so they can be used in SNI FQDN headers for the Connect Gateways
	// feature.
	//
	// This may be empty, in which case only the unnamed default subset
	// will be usable.
	Subsets map[string]ServiceResolverSubset `json:",omitempty"`

	// Redirect is a service/subset/datacenter/namespace to resolve
	// instead of the requested service (optional).
	//
	// When configured, all occurrences of this resolver in any discovery
	// chain evaluation will be substituted for the supplied redirect
	// EXCEPT when the redirect has already been applied.
	//
	// When substituting the supplied redirect into the discovery chain
	// all other fields beside Kind/Name/Redirect will be ignored.
	Redirect *ServiceResolverRedirect `json:",omitempty"`

	// Failover controls when and how to reroute traffic to an alternate pool
	// of service instances.
	//
	// The map is keyed by the service subset it applies to, and the special
	// string "*" is a wildcard that applies to any subset not otherwise
	// specified here.
	Failover map[string]ServiceResolverFailover `json:",omitempty"`

	// ConnectTimeout is the timeout for establishing new network connections
	// to this service.
	ConnectTimeout time.Duration `json:",omitempty" alias:"connect_timeout"`

	// LoadBalancer determines the load balancing policy and configuration for services
	// issuing requests to this upstream service.
	LoadBalancer *LoadBalancer `json:",omitempty" alias:"load_balancer"`

	Meta           map[string]string `json:",omitempty"`
	EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
	RaftIndex
}

func (e *ServiceResolverConfigEntry) MarshalJSON() ([]byte, error) {
	type Alias ServiceResolverConfigEntry
	exported := &struct {
		ConnectTimeout string `json:",omitempty"`
		*Alias
	}{
		ConnectTimeout: e.ConnectTimeout.String(),
		Alias:          (*Alias)(e),
	}
	if e.ConnectTimeout == 0 {
		exported.ConnectTimeout = ""
	}

	return json.Marshal(exported)
}

func (e *ServiceResolverConfigEntry) UnmarshalJSON(data []byte) error {
	type Alias ServiceResolverConfigEntry
	aux := &struct {
		ConnectTimeout string
		*Alias
	}{
		Alias: (*Alias)(e),
	}
	if err := lib.UnmarshalJSON(data, &aux); err != nil {
		return err
	}
	var err error
	if aux.ConnectTimeout != "" {
		if e.ConnectTimeout, err = time.ParseDuration(aux.ConnectTimeout); err != nil {
			return err
		}
	}
	return nil
}

func (e *ServiceResolverConfigEntry) SubsetExists(name string) bool {
	if name == "" {
		return true
	}
	if len(e.Subsets) == 0 {
		return false
	}
	_, ok := e.Subsets[name]
	return ok
}

func (e *ServiceResolverConfigEntry) IsDefault() bool {
	return e.DefaultSubset == "" &&
		len(e.Subsets) == 0 &&
		e.Redirect == nil &&
		len(e.Failover) == 0 &&
		e.ConnectTimeout == 0 &&
		e.LoadBalancer == nil
}

func (e *ServiceResolverConfigEntry) GetKind() string {
	return ServiceResolver
}

func (e *ServiceResolverConfigEntry) GetName() string {
	if e == nil {
		return ""
	}

	return e.Name
}

func (e *ServiceResolverConfigEntry) GetMeta() map[string]string {
	if e == nil {
		return nil
	}
	return e.Meta
}

func (e *ServiceResolverConfigEntry) Normalize() error {
	if e == nil {
		return fmt.Errorf("config entry is nil")
	}

	e.Kind = ServiceResolver

	e.EnterpriseMeta.Normalize()

	return nil
}

func (e *ServiceResolverConfigEntry) Validate() error {
	if e.Name == "" {
		return fmt.Errorf("Name is required")
	}

	if err := validateConfigEntryMeta(e.Meta); err != nil {
		return err
	}

	if len(e.Subsets) > 0 {
		for name := range e.Subsets {
			if name == "" {
				return fmt.Errorf("Subset defined with empty name")
			}
			if err := validateServiceSubset(name); err != nil {
				return fmt.Errorf("Subset %q is invalid: %v", name, err)
			}
		}
	}

	isSubset := func(subset string) bool {
		if len(e.Subsets) > 0 {
			_, ok := e.Subsets[subset]
			return ok
		}
		return false
	}

	if e.DefaultSubset != "" && !isSubset(e.DefaultSubset) {
		return fmt.Errorf("DefaultSubset %q is not a valid subset", e.DefaultSubset)
	}

	if e.Redirect != nil {
		r := e.Redirect

		if len(e.Failover) > 0 {
			return fmt.Errorf("Redirect and Failover cannot both be set")
		}

		// TODO(rb): prevent subsets and default subsets from being defined?

		if r.Service == "" && r.ServiceSubset == "" && r.Namespace == "" && r.Datacenter == "" {
			return fmt.Errorf("Redirect is empty")
		}

		if r.Service == "" {
			if r.ServiceSubset != "" {
				return fmt.Errorf("Redirect.ServiceSubset defined without Redirect.Service")
			}
			if r.Namespace != "" {
				return fmt.Errorf("Redirect.Namespace defined without Redirect.Service")
			}
		} else if r.Service == e.Name {
			if r.ServiceSubset != "" && !isSubset(r.ServiceSubset) {
				return fmt.Errorf("Redirect.ServiceSubset %q is not a valid subset of %q", r.ServiceSubset, r.Service)
			}
		}
	}

	if len(e.Failover) > 0 {
		for subset, f := range e.Failover {
			if subset != "*" && !isSubset(subset) {
				return fmt.Errorf("Bad Failover[%q]: not a valid subset", subset)
			}

			if f.Service == "" && f.ServiceSubset == "" && f.Namespace == "" && len(f.Datacenters) == 0 {
				return fmt.Errorf("Bad Failover[%q] one of Service, ServiceSubset, Namespace, or Datacenters is required", subset)
			}

			if f.ServiceSubset != "" {
				if f.Service == "" || f.Service == e.Name {
					if !isSubset(f.ServiceSubset) {
						return fmt.Errorf("Bad Failover[%q].ServiceSubset %q is not a valid subset of %q", subset, f.ServiceSubset, f.Service)
					}
				}
			}

			for _, dc := range f.Datacenters {
				if dc == "" {
					return fmt.Errorf("Bad Failover[%q].Datacenters: found empty datacenter", subset)
				}
			}
		}
	}

	if e.ConnectTimeout < 0 {
		return fmt.Errorf("Bad ConnectTimeout '%s', must be >= 0", e.ConnectTimeout)
	}

	if e.LoadBalancer != nil {
		lb := e.LoadBalancer

		if ok := validLBPolicies[lb.Policy]; !ok {
			return fmt.Errorf("Bad LoadBalancer policy: %q is not supported", lb.Policy)
		}

		if lb.Policy != LBPolicyRingHash && lb.RingHashConfig != nil {
			return fmt.Errorf("Bad LoadBalancer configuration. "+
				"RingHashConfig specified for incompatible load balancing policy %q", lb.Policy)
		}
		if lb.Policy != LBPolicyLeastRequest && lb.LeastRequestConfig != nil {
			return fmt.Errorf("Bad LoadBalancer configuration. "+
				"LeastRequestConfig specified for incompatible load balancing policy %q", lb.Policy)
		}
		if !lb.IsHashBased() && len(lb.HashPolicies) > 0 {
			return fmt.Errorf("Bad LoadBalancer configuration: "+
				"HashPolicies specified for non-hash-based Policy: %q", lb.Policy)
		}

		for i, hp := range lb.HashPolicies {
			if ok := validHashPolicies[hp.Field]; hp.Field != "" && !ok {
				return fmt.Errorf("Bad LoadBalancer HashPolicy[%d]: %q is not a supported field", i, hp.Field)
			}

			if hp.SourceIP && hp.Field != "" {
				return fmt.Errorf("Bad LoadBalancer HashPolicy[%d]: "+
					"A single hash policy cannot hash both a source address and a %q", i, hp.Field)
			}
			if hp.SourceIP && hp.FieldValue != "" {
				return fmt.Errorf("Bad LoadBalancer HashPolicy[%d]: "+
					"A FieldValue cannot be specified when hashing SourceIP", i)
			}
			if hp.Field != "" && hp.FieldValue == "" {
				return fmt.Errorf("Bad LoadBalancer HashPolicy[%d]: Field %q was specified without a FieldValue", i, hp.Field)
			}
			if hp.FieldValue != "" && hp.Field == "" {
				return fmt.Errorf("Bad LoadBalancer HashPolicy[%d]: FieldValue requires a Field to apply to", i)
			}
			if hp.CookieConfig != nil {
				if hp.Field != HashPolicyCookie {
					return fmt.Errorf("Bad LoadBalancer HashPolicy[%d]: cookie_config provided for %q", i, hp.Field)
				}
				if hp.CookieConfig.Session && hp.CookieConfig.TTL != 0*time.Second {
					return fmt.Errorf("Bad LoadBalancer HashPolicy[%d]: a session cookie cannot have an associated TTL", i)
				}
			}
		}
	}

	return nil
}

func (e *ServiceResolverConfigEntry) CanRead(rule acl.Authorizer) bool {
	return canReadDiscoveryChain(e, rule)
}

func (e *ServiceResolverConfigEntry) CanWrite(rule acl.Authorizer) bool {
	return canWriteDiscoveryChain(e, rule)
}

func (e *ServiceResolverConfigEntry) GetRaftIndex() *RaftIndex {
	if e == nil {
		return &RaftIndex{}
	}

	return &e.RaftIndex
}

func (e *ServiceResolverConfigEntry) GetEnterpriseMeta() *EnterpriseMeta {
	if e == nil {
		return nil
	}

	return &e.EnterpriseMeta
}

func (e *ServiceResolverConfigEntry) ListRelatedServices() []ServiceID {
	found := make(map[ServiceID]struct{})

	svcID := NewServiceID(e.Name, &e.EnterpriseMeta)
	if e.Redirect != nil {
		redirectID := NewServiceID(defaultIfEmpty(e.Redirect.Service, e.Name), e.Redirect.GetEnterpriseMeta(&e.EnterpriseMeta))
		if redirectID != svcID {
			found[redirectID] = struct{}{}
		}
	}

	if len(e.Failover) > 0 {
		for _, failover := range e.Failover {
			failoverID := NewServiceID(defaultIfEmpty(failover.Service, e.Name), failover.GetEnterpriseMeta(&e.EnterpriseMeta))
			if failoverID != svcID {
				found[failoverID] = struct{}{}
			}
		}
	}

	if len(found) == 0 {
		return nil
	}

	out := make([]ServiceID, 0, len(found))
	for svc := range found {
		out = append(out, svc)
	}
	sort.Slice(out, func(i, j int) bool {
		return out[i].EnterpriseMeta.LessThan(&out[j].EnterpriseMeta) ||
			out[i].ID < out[j].ID
	})
	return out
}

// ServiceResolverSubset defines a way to select a portion of the Consul
// catalog during service discovery. Anything that affects the ultimate catalog
// query performed OR post-processing on the results of that sort of query
// should be defined here.
type ServiceResolverSubset struct {
	// Filter specifies the go-bexpr filter expression to be used for selecting
	// instances of the requested service.
	Filter string `json:",omitempty"`

	// OnlyPassing - Specifies the behavior of the resolver's health check
	// filtering. If this is set to false, the results will include instances
	// with checks in the passing as well as the warning states. If this is set
	// to true, only instances with checks in the passing state will be
	// returned. (behaves identically to the similarly named field on prepared
	// queries).
	OnlyPassing bool `json:",omitempty" alias:"only_passing"`
}

type ServiceResolverRedirect struct {
	// Service is a service to resolve instead of the current service
	// (optional).
	Service string `json:",omitempty"`

	// ServiceSubset is a named subset of the given service to resolve instead
	// of one defined as that service's DefaultSubset If empty the default
	// subset is used (optional).
	//
	// If this is specified at least one of Service, Datacenter, or Namespace
	// should be configured.
	ServiceSubset string `json:",omitempty" alias:"service_subset"`

	// Namespace is the namespace to resolve the service from instead of the
	// current one (optional).
	Namespace string `json:",omitempty"`

	// Datacenter is the datacenter to resolve the service from instead of the
	// current one (optional).
	Datacenter string `json:",omitempty"`
}

// There are some restrictions on what is allowed in here:
//
// - Service, ServiceSubset, Namespace, and Datacenters cannot all be
//   empty at once.
//
type ServiceResolverFailover struct {
	// Service is the service to resolve instead of the default as the failover
	// group of instances (optional).
	//
	// This is a DESTINATION during failover.
	Service string `json:",omitempty"`

	// ServiceSubset is the named subset of the requested service to resolve as
	// the failover group of instances. If empty the default subset for the
	// requested service is used (optional).
	//
	// This is a DESTINATION during failover.
	ServiceSubset string `json:",omitempty" alias:"service_subset"`

	// Namespace is the namespace to resolve the requested service from to form
	// the failover group of instances. If empty the current namespace is used
	// (optional).
	//
	// This is a DESTINATION during failover.
	Namespace string `json:",omitempty"`

	// Datacenters is a fixed list of datacenters to try. We never try a
	// datacenter multiple times, so those are subtracted from this list before
	// proceeding.
	//
	// This is a DESTINATION during failover.
	Datacenters []string `json:",omitempty"`
}

// LoadBalancer determines the load balancing policy and configuration for services
// issuing requests to this upstream service.
type LoadBalancer struct {
	// Policy is the load balancing policy used to select a host
	Policy string `json:",omitempty"`

	// RingHashConfig contains configuration for the "ring_hash" policy type
	RingHashConfig *RingHashConfig `json:",omitempty" alias:"ring_hash_config"`

	// LeastRequestConfig contains configuration for the "least_request" policy type
	LeastRequestConfig *LeastRequestConfig `json:",omitempty" alias:"least_request_config"`

	// HashPolicies is a list of hash policies to use for hashing load balancing algorithms.
	// Hash policies are evaluated individually and combined such that identical lists
	// result in the same hash.
	// If no hash policies are present, or none are successfully evaluated,
	// then a random backend host will be selected.
	HashPolicies []HashPolicy `json:",omitempty" alias:"hash_policies"`
}

// RingHashConfig contains configuration for the "ring_hash" policy type
type RingHashConfig struct {
	// MinimumRingSize determines the minimum number of entries in the hash ring
	MinimumRingSize uint64 `json:",omitempty" alias:"minimum_ring_size"`

	// MaximumRingSize determines the maximum number of entries in the hash ring
	MaximumRingSize uint64 `json:",omitempty" alias:"maximum_ring_size"`
}

// LeastRequestConfig contains configuration for the "least_request" policy type
type LeastRequestConfig struct {
	// ChoiceCount determines the number of random healthy hosts from which to select the one with the least requests.
	ChoiceCount uint32 `json:",omitempty" alias:"choice_count"`
}

// HashPolicy defines which attributes will be hashed by hash-based LB algorithms
type HashPolicy struct {
	// Field is the attribute type to hash on.
	// Must be one of "header","cookie", or "query_parameter".
	// Cannot be specified along with SourceIP.
	Field string `json:",omitempty"`

	// FieldValue is the value to hash.
	// ie. header name, cookie name, URL query parameter name
	// Cannot be specified along with SourceIP.
	FieldValue string `json:",omitempty" alias:"field_value"`

	// CookieConfig contains configuration for the "cookie" hash policy type.
	CookieConfig *CookieConfig `json:",omitempty" alias:"cookie_config"`

	// SourceIP determines whether the hash should be of the source IP rather than of a field and field value.
	// Cannot be specified along with Field or FieldValue.
	SourceIP bool `json:",omitempty" alias:"source_ip"`

	// Terminal will short circuit the computation of the hash when multiple hash policies are present.
	// If a hash is computed when a Terminal policy is evaluated,
	// then that hash will be used and subsequent hash policies will be ignored.
	Terminal bool `json:",omitempty"`
}

// CookieConfig contains configuration for the "cookie" hash policy type.
// This is specified to have Envoy generate a cookie for a client on its first request.
type CookieConfig struct {
	// Generates a session cookie with no expiration.
	Session bool `json:",omitempty"`

	// TTL for generated cookies. Cannot be specified for session cookies.
	TTL time.Duration `json:",omitempty"`

	// The path to set for the cookie
	Path string `json:",omitempty"`
}

func (lb *LoadBalancer) IsHashBased() bool {
	if lb == nil {
		return false
	}

	switch lb.Policy {
	case LBPolicyMaglev, LBPolicyRingHash:
		return true
	default:
		return false
	}
}

type discoveryChainConfigEntry interface {
	ConfigEntry
	// ListRelatedServices returns a list of other names of services referenced
	// in this config entry.
	ListRelatedServices() []ServiceID
}

func canReadDiscoveryChain(entry discoveryChainConfigEntry, authz acl.Authorizer) bool {
	var authzContext acl.AuthorizerContext
	entry.GetEnterpriseMeta().FillAuthzContext(&authzContext)
	return authz.ServiceRead(entry.GetName(), &authzContext) == acl.Allow
}

func canWriteDiscoveryChain(entry discoveryChainConfigEntry, rule acl.Authorizer) bool {
	entryID := NewServiceID(entry.GetName(), entry.GetEnterpriseMeta())

	var authzContext acl.AuthorizerContext
	entryID.FillAuthzContext(&authzContext)

	name := entry.GetName()

	if rule.ServiceWrite(name, &authzContext) != acl.Allow {
		return false
	}

	for _, svc := range entry.ListRelatedServices() {
		if entryID == svc {
			continue
		}

		svc.FillAuthzContext(&authzContext)
		// You only need read on related services to redirect traffic flow for
		// your own service.
		if rule.ServiceRead(svc.ID, &authzContext) != acl.Allow {
			return false
		}
	}
	return true
}

// DiscoveryChainRequest is used when requesting the discovery chain for a
// service.
type DiscoveryChainRequest struct {
	Name                 string
	EvaluateInDatacenter string
	EvaluateInNamespace  string

	// OverrideMeshGateway allows for the mesh gateway setting to be overridden
	// for any resolver in the compiled chain.
	OverrideMeshGateway MeshGatewayConfig

	// OverrideProtocol allows for the final protocol for the chain to be
	// altered.
	//
	// - If the chain ordinarily would be TCP and an L7 protocol is passed here
	// the chain will not include Routers or Splitters.
	//
	// - If the chain ordinarily would be L7 and TCP is passed here the chain
	// will not include Routers or Splitters.
	OverrideProtocol string

	// OverrideConnectTimeout allows for the ConnectTimeout setting to be
	// overridden for any resolver in the compiled chain.
	OverrideConnectTimeout time.Duration

	Datacenter string // where to route the RPC
	QueryOptions
}

func (r *DiscoveryChainRequest) RequestDatacenter() string {
	return r.Datacenter
}

func (r *DiscoveryChainRequest) CacheInfo() cache.RequestInfo {
	info := cache.RequestInfo{
		Token:          r.Token,
		Datacenter:     r.Datacenter,
		MinIndex:       r.MinQueryIndex,
		Timeout:        r.MaxQueryTime,
		MaxAge:         r.MaxAge,
		MustRevalidate: r.MustRevalidate,
	}

	v, err := hashstructure.Hash(struct {
		Name                   string
		EvaluateInDatacenter   string
		EvaluateInNamespace    string
		OverrideMeshGateway    MeshGatewayConfig
		OverrideProtocol       string
		OverrideConnectTimeout time.Duration
		Filter                 string
	}{
		Name:                   r.Name,
		EvaluateInDatacenter:   r.EvaluateInDatacenter,
		EvaluateInNamespace:    r.EvaluateInNamespace,
		OverrideMeshGateway:    r.OverrideMeshGateway,
		OverrideProtocol:       r.OverrideProtocol,
		OverrideConnectTimeout: r.OverrideConnectTimeout,
		Filter:                 r.QueryOptions.Filter,
	}, nil)
	if err == nil {
		// If there is an error, we don't set the key. A blank key forces
		// no cache for this request so the request is forwarded directly
		// to the server.
		info.Key = strconv.FormatUint(v, 10)
	}

	return info
}

type DiscoveryChainResponse struct {
	Chain *CompiledDiscoveryChain
	QueryMeta
}

type ConfigEntryGraphError struct {
	// one of Message or Err should be set
	Message string
	Err     error
}

func (e *ConfigEntryGraphError) Error() string {
	if e.Err != nil {
		return e.Err.Error()
	}
	return e.Message
}

var (
	validServiceSubset     = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`)
	serviceSubsetMaxLength = 63
)

// validateServiceSubset checks if the provided name can be used as an service
// subset. Because these are used in SNI headers they must a DNS label per
// RFC-1035/RFC-1123.
func validateServiceSubset(subset string) error {
	if subset == "" || len(subset) > serviceSubsetMaxLength {
		return fmt.Errorf("must be non-empty and 63 characters or fewer")
	}
	if !validServiceSubset.MatchString(subset) {
		return fmt.Errorf("must be 63 characters or fewer, begin or end with lower case alphanumeric characters, and contain lower case alphanumeric characters or '-' in between")
	}
	return nil
}

func defaultIfEmpty(val, defaultVal string) string {
	if val != "" {
		return val
	}
	return defaultVal
}

func IsProtocolHTTPLike(protocol string) bool {
	switch protocol {
	case "http", "http2", "grpc":
		return true
	default:
		return false
	}
}
