// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package consul

import (
	"fmt"
	"strings"
	"time"

	"github.com/armon/go-metrics"
	"github.com/armon/go-metrics/prometheus"
	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/go-memdb"
	"github.com/hashicorp/go-uuid"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/agent/consul/state"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/agent/structs/aclfilter"
	"github.com/hashicorp/consul/logging"
)

var PreparedQuerySummaries = []prometheus.SummaryDefinition{
	{
		Name: []string{"prepared-query", "apply"},
		Help: "Measures the time it takes to apply a prepared query update.",
	},
	{
		Name: []string{"prepared-query", "explain"},
		Help: "Measures the time it takes to process a prepared query explain request.",
	},
	{
		Name: []string{"prepared-query", "execute"},
		Help: "Measures the time it takes to process a prepared query execute request.",
	},
	{
		Name: []string{"prepared-query", "execute_remote"},
		Help: "Measures the time it takes to process a prepared query execute request that was forwarded to another datacenter.",
	},
}

// PreparedQuery manages the prepared query endpoint.
type PreparedQuery struct {
	srv    *Server
	logger hclog.Logger
}

// Apply is used to apply a modifying request to the data store. This should
// only be used for operations that modify the data. The ID of the session is
// returned in the reply.
func (p *PreparedQuery) Apply(args *structs.PreparedQueryRequest, reply *string) (err error) {
	if done, err := p.srv.ForwardRPC("PreparedQuery.Apply", args, reply); done {
		return err
	}
	defer metrics.MeasureSince([]string{"prepared-query", "apply"}, time.Now())

	// Validate the ID. We must create new IDs before applying to the Raft
	// log since it's not deterministic.
	if args.Op == structs.PreparedQueryCreate {
		if args.Query.ID != "" {
			return fmt.Errorf("ID must be empty when creating a new prepared query")
		}

		// We are relying on the fact that UUIDs are random and unlikely
		// to collide since this isn't inside a write transaction.
		state := p.srv.fsm.State()
		for {
			if args.Query.ID, err = uuid.GenerateUUID(); err != nil {
				return fmt.Errorf("UUID generation for prepared query failed: %v", err)
			}
			_, query, err := state.PreparedQueryGet(nil, args.Query.ID)
			if err != nil {
				return fmt.Errorf("Prepared query lookup failed: %v", err)
			}
			if query == nil {
				break
			}
		}
	}
	*reply = args.Query.ID

	// Get the ACL token for the request for the checks below.
	authz, err := p.srv.ResolveToken(args.Token)
	if err != nil {
		return err
	}

	// If prefix ACLs apply to the incoming query, then do an ACL check. We
	// need to make sure they have write access for whatever they are
	// proposing.
	if prefix, ok := args.Query.GetACLPrefix(); ok {
		if err := authz.ToAllowAuthorizer().PreparedQueryWriteAllowed(prefix, nil); err != nil {
			p.logger.Warn("Operation on prepared query denied due to ACLs", "query", args.Query.ID)
			return err
		}
	}

	// This is the second part of the check above. If they are referencing
	// an existing query then make sure it exists and that they have write
	// access to whatever they are changing, if prefix ACLs apply to it.
	if args.Op != structs.PreparedQueryCreate {
		state := p.srv.fsm.State()
		_, query, err := state.PreparedQueryGet(nil, args.Query.ID)
		if err != nil {
			return fmt.Errorf("Prepared Query lookup failed: %v", err)
		}
		if query == nil {
			return fmt.Errorf("Cannot modify non-existent prepared query: '%s'", args.Query.ID)
		}

		if prefix, ok := query.GetACLPrefix(); ok {
			if err := authz.ToAllowAuthorizer().PreparedQueryWriteAllowed(prefix, nil); err != nil {
				p.logger.Warn("Operation on prepared query denied due to ACLs", "query", args.Query.ID)
				return err
			}
		}
	}

	// Parse the query and prep it for the state store.
	switch args.Op {
	case structs.PreparedQueryCreate, structs.PreparedQueryUpdate:
		if err := parseQuery(args.Query); err != nil {
			return fmt.Errorf("Invalid prepared query: %v", err)
		}

	case structs.PreparedQueryDelete:
		// Nothing else to verify here, just do the delete (we only look
		// at the ID field for this op).

	default:
		return fmt.Errorf("Unknown prepared query operation: %s", args.Op)
	}

	// Commit the query to the state store.
	_, err = p.srv.raftApply(structs.PreparedQueryRequestType, args)
	if err != nil {
		return fmt.Errorf("raft apply failed: %w", err)
	}
	return nil
}

// parseQuery makes sure the entries of a query are valid for a create or
// update operation. Some fields are not checked or are partially
// checked, as noted in the comments below. This also updates all the parsed
// fields of the query.
func parseQuery(query *structs.PreparedQuery) error {
	// We skip a few fields:
	// - ID is checked outside this fn.
	// - Name is optional with no restrictions, except for uniqueness which
	//   is checked for integrity during the transaction. We also make sure
	//   names do not overlap with IDs, which is also checked during the
	//   transaction. Otherwise, people could "steal" queries that they don't
	//   have proper ACL rights to change.
	// - Template is checked during the transaction since that's where we
	//   compile it.

	// Anonymous queries require a session or need to be part of a template.
	if query.Name == "" && query.Template.Type == "" && query.Session == "" {
		return fmt.Errorf("Must be bound to a session")
	}

	// Token is checked when the query is executed, but we do make sure the
	// user hasn't accidentally pasted-in the special redacted token name,
	// which if we allowed in would be super hard to debug and understand.
	if query.Token == aclfilter.RedactedToken {
		return fmt.Errorf("Bad Token '%s', it looks like a query definition with a redacted token was submitted", query.Token)
	}

	// Parse the service query sub-structure.
	if err := parseService(&query.Service); err != nil {
		return err
	}

	// Parse the DNS options sub-structure.
	if err := parseDNS(&query.DNS); err != nil {
		return err
	}

	return nil
}

// parseService makes sure the entries of a query are valid for a create or
// update operation. Some of the fields are not checked or are partially
// checked, as noted in the comments below. This also updates all the parsed
// fields of the query.
func parseService(svc *structs.ServiceQuery) error {
	// Service is required.
	if svc.Service == "" {
		return fmt.Errorf("Must provide a Service name to query")
	}

	failover := svc.Failover
	// NearestN can be 0 which means "don't fail over by RTT".
	if failover.NearestN < 0 {
		return fmt.Errorf("Bad NearestN '%d', must be >= 0", svc.Failover.NearestN)
	}

	if (failover.NearestN != 0 || len(failover.Datacenters) != 0) && len(failover.Targets) != 0 {
		return fmt.Errorf("Targets cannot be populated with NearestN or Datacenters")
	}

	// Make sure the metadata filters are valid
	if err := structs.ValidateNodeMetadata(svc.NodeMeta, true); err != nil {
		return err
	}

	if err := parseSameness(svc); err != nil {
		return err
	}

	// We skip a few fields:
	// - There's no validation for Datacenters; we skip any unknown entries
	//   at execution time.
	// - OnlyPassing is just a boolean so doesn't need further validation.
	// - Tags is a free-form list of tags and doesn't need further validation.

	return nil
}

// parseDNS makes sure the entries of a query are valid for a create or
// update operation. This also updates all the parsed fields of the query.
func parseDNS(dns *structs.QueryDNSOptions) error {
	if dns.TTL != "" {
		ttl, err := time.ParseDuration(dns.TTL)
		if err != nil {
			return fmt.Errorf("Bad DNS TTL '%s': %v", dns.TTL, err)
		}

		if ttl < 0 {
			return fmt.Errorf("DNS TTL '%d', must be >=0", ttl)
		}
	}

	return nil
}

// Get returns a single prepared query by ID.
func (p *PreparedQuery) Get(args *structs.PreparedQuerySpecificRequest,
	reply *structs.IndexedPreparedQueries) error {
	if done, err := p.srv.ForwardRPC("PreparedQuery.Get", args, reply); done {
		return err
	}

	return p.srv.blockingQuery(
		&args.QueryOptions,
		&reply.QueryMeta,
		func(ws memdb.WatchSet, state *state.Store) error {
			index, query, err := state.PreparedQueryGet(ws, args.QueryID)
			if err != nil {
				return err
			}
			if query == nil {
				return structs.ErrQueryNotFound
			}

			// If no prefix ACL applies to this query, then they are
			// always allowed to see it if they have the ID. We still
			// have to filter the remaining object for tokens.
			reply.Index = index
			reply.Queries = structs.PreparedQueries{query}
			if _, ok := query.GetACLPrefix(); !ok {
				return p.srv.filterACL(args.Token, &reply.Queries[0])
			}

			// Otherwise, attempt to filter it the usual way.
			if err := p.srv.filterACL(args.Token, reply); err != nil {
				return err
			}

			// Since this is a GET of a specific query, if ACLs have
			// prevented us from returning something that exists,
			// then alert the user with a permission denied error.
			if len(reply.Queries) == 0 {
				p.logger.Warn("Request to get prepared query denied due to ACLs", "query", args.QueryID)
				return acl.ErrPermissionDenied
			}

			return nil
		})
}

// List returns all the prepared queries.
func (p *PreparedQuery) List(args *structs.DCSpecificRequest, reply *structs.IndexedPreparedQueries) error {
	if done, err := p.srv.ForwardRPC("PreparedQuery.List", args, reply); done {
		return err
	}

	return p.srv.blockingQuery(
		&args.QueryOptions,
		&reply.QueryMeta,
		func(ws memdb.WatchSet, state *state.Store) error {
			index, queries, err := state.PreparedQueryList(ws)
			if err != nil {
				return err
			}

			reply.Index, reply.Queries = index, queries
			return p.srv.filterACL(args.Token, reply)
		})
}

// Explain resolves a prepared query and returns the (possibly rendered template)
// to the caller. This is useful for letting operators figure out which query is
// picking up a given name. We can also add additional info about how the query
// will be executed here.
func (p *PreparedQuery) Explain(args *structs.PreparedQueryExecuteRequest,
	reply *structs.PreparedQueryExplainResponse) error {
	if done, err := p.srv.ForwardRPC("PreparedQuery.Explain", args, reply); done {
		return err
	}
	defer metrics.MeasureSince([]string{"prepared-query", "explain"}, time.Now())

	// We have to do this ourselves since we are not doing a blocking RPC.
	p.srv.SetQueryMeta(&reply.QueryMeta, args.Token)
	if args.RequireConsistent {
		if err := p.srv.ConsistentRead(); err != nil {
			return err
		}
	}

	// Try to locate the query.
	state := p.srv.fsm.State()
	_, query, err := state.PreparedQueryResolve(args.QueryIDOrName, args.Agent)
	if err != nil {
		return err
	}
	if query == nil {
		return structs.ErrQueryNotFound
	}

	// Place the query into a list so we can run the standard ACL filter on
	// it.
	queries := &structs.IndexedPreparedQueries{
		Queries: structs.PreparedQueries{query},
	}
	if err := p.srv.filterACL(args.Token, queries); err != nil {
		return err
	}

	// If the query was filtered out, return an error.
	if len(queries.Queries) == 0 {
		p.logger.Warn("Explain on prepared query denied due to ACLs", "query", query.ID)
		return acl.ErrPermissionDenied
	}

	reply.Query = *(queries.Queries[0])
	return nil
}

// Execute runs a prepared query and returns the results. This will perform the
// failover logic if no local results are available. This is typically called as
// part of a DNS lookup, or when executing prepared queries from the HTTP API.
func (p *PreparedQuery) Execute(args *structs.PreparedQueryExecuteRequest,
	reply *structs.PreparedQueryExecuteResponse) error {
	if done, err := p.srv.ForwardRPC("PreparedQuery.Execute", args, reply); done {
		return err
	}
	defer metrics.MeasureSince([]string{"prepared-query", "execute"}, time.Now())

	// We have to do this ourselves since we are not doing a blocking RPC.
	if args.RequireConsistent {
		if err := p.srv.ConsistentRead(); err != nil {
			return err
		}
	}

	// Try to locate the query.
	state := p.srv.fsm.State()
	_, query, err := state.PreparedQueryResolve(args.QueryIDOrName, args.Agent)
	if err != nil {
		return err
	}
	if query == nil {
		return structs.ErrQueryNotFound
	}

	// If we have a sameness group, it controls the initial query and
	// subsequent failover if required (Enterprise Only)
	if query.Service.SamenessGroup != "" {
		wrapper := newQueryServerWrapper(p.srv, p.ExecuteRemote)
		if err := querySameness(wrapper, *query, args, reply); err != nil {
			return err
		}
	} else {
		// Execute the query for the local DC.
		if err := p.execute(query, reply, args.Connect); err != nil {
			return err
		}

		// If they supplied a token with the query, use that, otherwise use the
		// token passed in with the request.
		token := args.QueryOptions.Token
		if query.Token != "" {
			token = query.Token
		}
		if err := p.srv.filterACL(token, reply); err != nil {
			return err
		}

		// TODO (slackpad) We could add a special case here that will avoid the
		// fail over if we filtered everything due to ACLs. This seems like it
		// might not be worth the code complexity and behavior differences,
		// though, since this is essentially a misconfiguration.

		// We have to do this ourselves since we are not doing a blocking RPC.
		p.srv.SetQueryMeta(&reply.QueryMeta, token)

		// Shuffle the results in case coordinates are not available if they
		// requested an RTT sort.
		reply.Nodes.Shuffle()

		// Build the query source. This can be provided by the client, or by
		// the prepared query. Client-specified takes priority.
		qs := args.Source
		if qs.Datacenter == "" {
			qs.Datacenter = args.Agent.Datacenter
		}
		if query.Service.Near != "" && qs.Node == "" {
			qs.Node = query.Service.Near
		}

		// Respect the magic "_agent" flag.
		if qs.Node == "_agent" {
			qs.Node = args.Agent.Node
		} else if qs.Node == "_ip" {
			if args.Source.Ip != "" {
				_, nodes, err := state.Nodes(nil, structs.NodeEnterpriseMetaInDefaultPartition(), structs.TODOPeerKeyword)
				if err != nil {
					return err
				}

				for _, node := range nodes {
					if args.Source.Ip == node.Address {
						qs.Node = node.Node
						break
					}
				}
			} else {
				p.logger.Warn("Prepared Query using near=_ip requires " +
					"the source IP to be set but none was provided. No distance " +
					"sorting will be done.")

			}

			// Either a source IP was given, but we couldn't find the associated node
			// or no source ip was given. In both cases we should wipe the Node value
			if qs.Node == "_ip" {
				qs.Node = ""
			}
		}

		// Perform the distance sort
		err = p.srv.sortNodesByDistanceFrom(qs, reply.Nodes)
		if err != nil {
			return err
		}

		// If we applied a distance sort, make sure that the node queried for is in
		// position 0, provided the results are from the same datacenter.
		if qs.Node != "" && reply.Datacenter == qs.Datacenter {
			for i, node := range reply.Nodes {
				if strings.EqualFold(node.Node.Node, qs.Node) {
					reply.Nodes[0], reply.Nodes[i] = reply.Nodes[i], reply.Nodes[0]
					break
				}

				// Put a cap on the depth of the search. The local agent should
				// never be further in than this if distance sorting was applied.
				if i == 9 {
					break
				}
			}
		}

		// Apply the limit if given.
		if args.Limit > 0 && len(reply.Nodes) > args.Limit {
			reply.Nodes = reply.Nodes[:args.Limit]
		}

		// In the happy path where we found some healthy nodes we go with that
		// and bail out. Otherwise, we fail over and try remote DCs, as allowed
		// by the query setup.
		if len(reply.Nodes) == 0 {
			wrapper := newQueryServerWrapper(p.srv, p.ExecuteRemote)
			if err := queryFailover(wrapper, *query, args, reply); err != nil {
				return err
			}
		}
	}

	return nil
}

// ExecuteRemote is used when a local node doesn't have any instances of a
// service available and needs to probe remote DCs. This sends the full query
// over since the remote side won't have it in its state store, and this doesn't
// do the failover logic since that's already being run on the originating DC.
// We don't want things to fan out further than one level.
func (p *PreparedQuery) ExecuteRemote(args *structs.PreparedQueryExecuteRemoteRequest,
	reply *structs.PreparedQueryExecuteResponse) error {
	if done, err := p.srv.ForwardRPC("PreparedQuery.ExecuteRemote", args, reply); done {
		return err
	}
	defer metrics.MeasureSince([]string{"prepared-query", "execute_remote"}, time.Now())

	// We have to do this ourselves since we are not doing a blocking RPC.
	if args.RequireConsistent {
		if err := p.srv.ConsistentRead(); err != nil {
			return err
		}
	}

	// Run the query locally to see what we can find.
	if err := p.execute(&args.Query, reply, args.Connect); err != nil {
		return err
	}

	// If they supplied a token with the query, use that, otherwise use the
	// token passed in with the request.
	token := args.QueryOptions.Token
	if args.Query.Token != "" {
		token = args.Query.Token
	}
	if err := p.srv.filterACL(token, reply); err != nil {
		return err
	}

	// We have to do this ourselves since we are not doing a blocking RPC.
	p.srv.SetQueryMeta(&reply.QueryMeta, token)

	// We don't bother trying to do an RTT sort here since we are by
	// definition in another DC. We just shuffle to make sure that we
	// balance the load across the results.
	reply.Nodes.Shuffle()

	// Apply the limit if given.
	if args.Limit > 0 && len(reply.Nodes) > args.Limit {
		reply.Nodes = reply.Nodes[:args.Limit]
	}

	return nil
}

// execute runs a prepared query in the local DC without any failover. We don't
// apply any sorting options or ACL checks at this level - it should be done up above.
func (p *PreparedQuery) execute(query *structs.PreparedQuery,
	reply *structs.PreparedQueryExecuteResponse,
	forceConnect bool) error {
	state := p.srv.fsm.State()

	// If we're requesting Connect-capable services, then switch the
	// lookup to be the Connect function.
	f := state.CheckServiceNodes
	if query.Service.Connect || forceConnect {
		f = state.CheckConnectServiceNodes
	}

	_, nodes, err := f(nil, query.Service.Service, &query.Service.EnterpriseMeta, query.Service.Peer)
	if err != nil {
		return err
	}

	// Filter out any unhealthy nodes.
	nodes = nodes.FilterIgnore(query.Service.OnlyPassing,
		query.Service.IgnoreCheckIDs)

	// Apply the node metadata filters, if any.
	if len(query.Service.NodeMeta) > 0 {
		nodes = nodeMetaFilter(query.Service.NodeMeta, nodes)
	}

	// Apply the service metadata filters, if any.
	if len(query.Service.ServiceMeta) > 0 {
		nodes = serviceMetaFilter(query.Service.ServiceMeta, nodes)
	}

	// Apply the tag filters, if any.
	if len(query.Service.Tags) > 0 {
		nodes = tagFilter(query.Service.Tags, nodes)
	}

	// Capture the nodes and pass the DNS information through to the reply.
	reply.Service = query.Service.Service
	reply.EnterpriseMeta = query.Service.EnterpriseMeta
	reply.Nodes = nodes
	reply.DNS = query.DNS

	// Stamp the result with its this datacenter or peer.
	if peerName := query.Service.Peer; peerName != "" {
		reply.PeerName = peerName
		reply.Datacenter = ""
	} else {
		reply.Datacenter = p.srv.config.Datacenter
	}

	return nil
}

// tagFilter returns a list of nodes who satisfy the given tags. Nodes must have
// ALL the given tags, and NONE of the forbidden tags (prefixed with !). Note
// for performance this modifies the original slice.
func tagFilter(tags []string, nodes structs.CheckServiceNodes) structs.CheckServiceNodes {
	// Build up lists of required and disallowed tags.
	must, not := make([]string, 0), make([]string, 0)
	for _, tag := range tags {
		tag = strings.ToLower(tag)
		if strings.HasPrefix(tag, "!") {
			tag = tag[1:]
			not = append(not, tag)
		} else {
			must = append(must, tag)
		}
	}

	n := len(nodes)
	for i := 0; i < n; i++ {
		node := nodes[i]

		// Index the tags so lookups this way are cheaper.
		index := make(map[string]struct{})
		if node.Service != nil {
			for _, tag := range node.Service.Tags {
				tag = strings.ToLower(tag)
				index[tag] = struct{}{}
			}
		}

		// Bail if any of the required tags are missing.
		for _, tag := range must {
			if _, ok := index[tag]; !ok {
				goto DELETE
			}
		}

		// Bail if any of the disallowed tags are present.
		for _, tag := range not {
			if _, ok := index[tag]; ok {
				goto DELETE
			}
		}

		// At this point, the service is ok to leave in the list.
		continue

	DELETE:
		nodes[i], nodes[n-1] = nodes[n-1], structs.CheckServiceNode{}
		n--
		i--
	}
	return nodes[:n]
}

// nodeMetaFilter returns a list of the nodes who satisfy the given metadata filters. Nodes
// must have ALL the given tags.
func nodeMetaFilter(filters map[string]string, nodes structs.CheckServiceNodes) structs.CheckServiceNodes {
	var filtered structs.CheckServiceNodes
	for _, node := range nodes {
		if structs.SatisfiesMetaFilters(node.Node.Meta, filters) {
			filtered = append(filtered, node)
		}
	}
	return filtered
}

func serviceMetaFilter(filters map[string]string, nodes structs.CheckServiceNodes) structs.CheckServiceNodes {
	var filtered structs.CheckServiceNodes
	for _, node := range nodes {
		if structs.SatisfiesMetaFilters(node.Service.Meta, filters) {
			filtered = append(filtered, node)
		}
	}
	return filtered
}

type stateLookuper interface {
	samenessGroupLookup(name string, entMeta acl.EnterpriseMeta) (uint64, *structs.SamenessGroupConfigEntry, error)
}

type stateLookup struct {
	srv *Server
}

// queryServer is a wrapper that makes it easier to test the failover logic.
type queryServer interface {
	GetLogger() hclog.Logger
	GetOtherDatacentersByDistance() ([]string, error)
	GetLocalDC() string
	ExecuteRemote(args *structs.PreparedQueryExecuteRemoteRequest, reply *structs.PreparedQueryExecuteResponse) error
	GetSamenessGroupFailoverTargets(name string, entMeta acl.EnterpriseMeta) ([]structs.QueryFailoverTarget, error)
}

// queryServerWrapper applies the queryServer interface to a Server.
type queryServerWrapper struct {
	srv           *Server
	sl            stateLookuper
	executeRemote func(args *structs.PreparedQueryExecuteRemoteRequest, reply *structs.PreparedQueryExecuteResponse) error
}

func newQueryServerWrapper(srv *Server, executeRemote func(args *structs.PreparedQueryExecuteRemoteRequest, reply *structs.PreparedQueryExecuteResponse) error) *queryServerWrapper {
	return &queryServerWrapper{
		srv:           srv,
		executeRemote: executeRemote,
		sl:            stateLookup{srv},
	}
}

// GetLocalDC returns the name of the local datacenter.
func (q *queryServerWrapper) GetLocalDC() string {
	return q.srv.config.Datacenter
}

// ExecuteRemote calls ExecuteRemote on PreparedQuery.
func (q *queryServerWrapper) ExecuteRemote(args *structs.PreparedQueryExecuteRemoteRequest, reply *structs.PreparedQueryExecuteResponse) error {
	return q.executeRemote(args, reply)
}

// GetLogger returns the server's logger.
func (q *queryServerWrapper) GetLogger() hclog.Logger {
	return q.srv.loggers.Named(logging.PreparedQuery)
}

// GetOtherDatacentersByDistance calls into the server's fn and filters out the
// server's own DC.
func (q *queryServerWrapper) GetOtherDatacentersByDistance() ([]string, error) {
	// TODO (slackpad) - We should cache this result since it's expensive to
	// compute.
	dcs, err := q.srv.router.GetDatacentersByDistance()
	if err != nil {
		return nil, err
	}

	var result []string
	for _, dc := range dcs {
		if dc != q.srv.config.Datacenter {
			result = append(result, dc)
		}
	}
	return result, nil
}

// queryFailover runs an algorithm to determine which DCs to try and then calls
// them to try to locate alternative services.
func queryFailover(q queryServer, query structs.PreparedQuery,
	args *structs.PreparedQueryExecuteRequest,
	reply *structs.PreparedQueryExecuteResponse) error {

	// Pull the list of other DCs. This is sorted by RTT in case the user
	// has selected that.
	nearest, err := q.GetOtherDatacentersByDistance()
	if err != nil {
		return err
	}

	// This will help us filter unknown targets supplied by the user.
	known := make(map[string]struct{})
	for _, dc := range nearest {
		known[dc] = struct{}{}
	}

	// Build a candidate list of failover targets to try, starting with the nearest N target
	// from RTTs.
	var targets []structs.QueryFailoverTarget
	index := make(map[string]struct{})
	if query.Service.Failover.NearestN > 0 {
		for i, dc := range nearest {
			if !(i < query.Service.Failover.NearestN) {
				break
			}

			targets = append(targets, structs.QueryFailoverTarget{Datacenter: dc})
			index[dc] = struct{}{}
		}
	}

	// Then add any targets explicitly listed that weren't selected above.
	for _, target := range query.Service.Failover.AsTargets() {
		// This will prevent a log of other log spam if we do not
		// attempt to talk to datacenters we don't know about.
		if dc := target.Datacenter; dc != "" {
			if _, ok := known[dc]; !ok {
				q.GetLogger().Debug("Skipping unknown datacenter in prepared query", "datacenter", dc)
				continue
			}

			// This will make sure we don't re-try something that fails
			// from the NearestN list.
			if _, ok := index[dc]; !ok {
				targets = append(targets, target)
				continue
			}
		}

		if target.Peer != "" || target.PartitionOrEmpty() != "" || target.NamespaceOrEmpty() != "" {
			targets = append(targets, target)
		}
	}

	// Now try the selected targets in priority order.
	failovers := 0
	for _, target := range targets {
		// This keeps track of how many iterations we actually run.
		failovers++

		err = targetSelector(q, query, args, target, reply)
		if err != nil {
			continue
		}

		// We can stop if we found some nodes.
		if len(reply.Nodes) > 0 {
			break
		}
	}

	// Set this at the end because the response from the remote doesn't have
	// this information.
	reply.Failovers = failovers

	return nil
}

func targetSelector(q queryServer,
	query structs.PreparedQuery,
	args *structs.PreparedQueryExecuteRequest,
	target structs.QueryFailoverTarget,
	reply *structs.PreparedQueryExecuteResponse) error {
	// Be super paranoid and set the nodes slice to nil since it's
	// the same slice we used before. We know there's nothing in
	// there, but the underlying msgpack library has a policy of
	// updating the slice when it's non-nil, and that feels dirty.
	// Let's just set it to nil so there's no way to communicate
	// through this slice across successive RPC calls.
	reply.Nodes = nil

	// Reset Peer, because it may have been set by a previous failover
	// target.
	query.Service.Peer = target.Peer
	query.Service.EnterpriseMeta = target.EnterpriseMeta
	dc := target.Datacenter
	if target.Peer != "" {
		dc = q.GetLocalDC()
	}

	// Note that we pass along the limit since may be applied
	// remotely to save bandwidth. We also pass along the consistency
	// mode information and token we were given, so that applies to
	// the remote query as well.
	remote := &structs.PreparedQueryExecuteRemoteRequest{
		Datacenter:   dc,
		Query:        query,
		Limit:        args.Limit,
		QueryOptions: args.QueryOptions,
		Connect:      args.Connect,
	}

	var err error
	if err = q.ExecuteRemote(remote, reply); err != nil {
		q.GetLogger().Warn("Failed querying for service in datacenter",
			"service", query.Service.Service,
			"peerName", query.Service.Peer,
			"datacenter", dc,
			"enterpriseMeta", query.Service.EnterpriseMeta,
			"error", err,
		)
		return err
	}

	return nil
}
