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

package structs

import (
	"strconv"

	"github.com/mitchellh/hashstructure"

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

// QueryFailoverOptions sets options about how we fail over if there are no
// healthy nodes in the local datacenter.
type QueryFailoverOptions struct {
	// NearestN is set to the number of remote datacenters to try, based on
	// network coordinates.
	NearestN int

	// Datacenters is a fixed list of datacenters to try after NearestN. We
	// never try a datacenter multiple times, so those are subtracted from
	// this list before proceeding.
	Datacenters []string

	// Targets is a fixed list of datacenters and peers to try. This field cannot
	// be populated with NearestN or Datacenters.
	Targets []QueryFailoverTarget
}

// AsTargets either returns Targets as is or Datacenters converted into
// Targets.
func (f *QueryFailoverOptions) AsTargets() []QueryFailoverTarget {
	if dcs := f.Datacenters; len(dcs) > 0 {
		var targets []QueryFailoverTarget
		for _, dc := range dcs {
			targets = append(targets, QueryFailoverTarget{Datacenter: dc})
		}
		return targets
	}

	return f.Targets
}

// IsEmpty returns true if the QueryFailoverOptions are empty (not set), false otherwise
func (f *QueryFailoverOptions) IsEmpty() bool {
	if f == nil || (f.NearestN == 0 && len(f.Datacenters) == 0 && len(f.Targets) == 0) {
		return true
	}
	return false
}

type QueryFailoverTarget struct {
	// Peer specifies a peer to try during failover.
	Peer string

	// Datacenter specifies a datacenter to try during failover.
	Datacenter string

	acl.EnterpriseMeta
}

// QueryDNSOptions controls settings when query results are served over DNS.
type QueryDNSOptions struct {
	// TTL is the time to live for the served DNS results.
	TTL string
}

// ServiceQuery is used to query for a set of healthy nodes offering a specific
// service.
type ServiceQuery struct {
	// Service is the service to query.
	Service string

	// SamenessGroup specifies a sameness group to query. The first member of the Sameness Group will
	// be targeted first on PQ execution and subsequent members will be targeted during failover scenarios.
	// This field is mutually exclusive with Failover.
	SamenessGroup string

	// Failover controls what we do if there are no healthy nodes in the
	// local datacenter.
	Failover QueryFailoverOptions

	// If OnlyPassing is true then we will only include nodes with passing
	// health checks (critical AND warning checks will cause a node to be
	// discarded)
	OnlyPassing bool

	// IgnoreCheckIDs is an optional list of health check IDs to ignore when
	// considering which nodes are healthy. It is useful as an emergency measure
	// to temporarily override some health check that is producing false negatives
	// for example.
	IgnoreCheckIDs []types.CheckID

	// Near allows the query to always prefer the node nearest the given
	// node. If the node does not exist, results are returned in their
	// normal randomly-shuffled order. Supplying the magic "_agent" value
	// is supported to sort near the agent which initiated the request.
	Near string

	// Tags are a set of required and/or disallowed tags. If a tag is in
	// this list it must be present. If the tag is preceded with "!" then
	// it is disallowed.
	Tags []string

	// NodeMeta is a map of required node metadata fields. If a key/value
	// pair is in this map it must be present on the node in order for the
	// service entry to be returned.
	NodeMeta map[string]string

	// ServiceMeta is a map of required service metadata fields. If a key/value
	// pair is in this map it must be present on the node in order for the
	// service entry to be returned.
	ServiceMeta map[string]string

	// Connect if true will filter the prepared query results to only
	// include Connect-capable services. These include both native services
	// and proxies for matching services. Note that if a proxy matches,
	// the constraints in the query above (Near, OnlyPassing, etc.) apply
	// to the _proxy_ and not the service being proxied. In practice, proxies
	// should be directly next to their services so this isn't an issue.
	Connect bool

	// If not empty, Peer represents the peer that the service
	// was imported from.
	Peer string

	// EnterpriseMeta is the embedded enterprise metadata
	acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
}

const (
	// QueryTemplateTypeNamePrefixMatch uses the Name field of the query as
	// a prefix to select the template.
	QueryTemplateTypeNamePrefixMatch = "name_prefix_match"
)

// QueryTemplateOptions controls settings if this query is a template.
type QueryTemplateOptions struct {
	// Type, if non-empty, means that this query is a template. This is
	// set to one of the QueryTemplateType* constants above.
	Type string

	// Regexp is an optional regular expression to use to parse the full
	// name, once the prefix match has selected a template. This can be
	// used to extract parts of the name and choose a service name, set
	// tags, etc.
	Regexp string

	// RemoveEmptyTags, if true, removes empty tags from matched tag list
	RemoveEmptyTags bool
}

// PreparedQuery defines a complete prepared query, and is the structure we
// maintain in the state store.
type PreparedQuery struct {
	// ID is this UUID-based ID for the query, always generated by Consul.
	ID string

	// Name is an optional friendly name for the query supplied by the
	// user. NOTE - if this feature is used then it will reduce the security
	// of any read ACL associated with this query/service since this name
	// can be used to locate nodes with supplying any ACL.
	Name string

	// Session is an optional session to tie this query's lifetime to. If
	// this is omitted then the query will not expire.
	Session string

	// Token is the ACL token used when the query was created, and it is
	// used when a query is subsequently executed. This token, or a token
	// with management privileges, must be used to change the query later.
	Token string

	// Template is used to configure this query as a template, which will
	// respond to queries based on the Name, and then will be rendered
	// before it is executed.
	Template QueryTemplateOptions

	// Service defines a service query (leaving things open for other types
	// later).
	Service ServiceQuery

	// DNS has options that control how the results of this query are
	// served over DNS.
	DNS QueryDNSOptions

	RaftIndex
}

// GetACLPrefix returns the prefix to look up the prepared_query ACL policy for
// this query, and whether the prefix applies to this query. You always need to
// check the ok value before using the prefix.
func (pq *PreparedQuery) GetACLPrefix() (string, bool) {
	if pq.Name != "" || pq.Template.Type != "" {
		return pq.Name, true
	}

	return "", false
}

type PreparedQueries []*PreparedQuery

type IndexedPreparedQueries struct {
	Queries PreparedQueries
	QueryMeta
}

type PreparedQueryOp string

const (
	PreparedQueryCreate PreparedQueryOp = "create"
	PreparedQueryUpdate PreparedQueryOp = "update"
	PreparedQueryDelete PreparedQueryOp = "delete"
)

// QueryRequest is used to create or change prepared queries.
type PreparedQueryRequest struct {
	// Datacenter is the target this request is intended for.
	Datacenter string

	// Op is the operation to apply.
	Op PreparedQueryOp

	// Query is the query itself.
	Query *PreparedQuery

	// WriteRequest holds the ACL token to go along with this request.
	WriteRequest
}

// RequestDatacenter returns the datacenter for a given request.
func (q *PreparedQueryRequest) RequestDatacenter() string {
	return q.Datacenter
}

// PreparedQuerySpecificRequest is used to get information about a prepared
// query.
type PreparedQuerySpecificRequest struct {
	// Datacenter is the target this request is intended for.
	Datacenter string

	// QueryID is the ID of a query.
	QueryID string

	// QueryOptions (unfortunately named here) controls the consistency
	// settings for the query lookup itself, as well as the service lookups.
	QueryOptions
}

// RequestDatacenter returns the datacenter for a given request.
func (q *PreparedQuerySpecificRequest) RequestDatacenter() string {
	return q.Datacenter
}

// PreparedQueryExecuteRequest is used to execute a prepared query.
type PreparedQueryExecuteRequest struct {
	// Datacenter is the target this request is intended for.
	Datacenter string

	// QueryIDOrName is the ID of a query _or_ the name of one, either can
	// be provided.
	QueryIDOrName string

	// Limit will trim the resulting list down to the given limit.
	Limit int

	// Connect will force results to be Connect-enabled nodes for the
	// matching services. This is equivalent in semantics exactly to
	// setting "Connect" in the query template itself, but allows callers
	// to use any prepared query in a Connect setting.
	Connect bool

	// Source is used to sort the results relative to a given node using
	// network coordinates.
	Source QuerySource

	// Agent is used to carry around a reference to the agent which initiated
	// the execute request. Used to distance-sort relative to the local node.
	Agent QuerySource

	// QueryOptions (unfortunately named here) controls the consistency
	// settings for the query lookup itself, as well as the service lookups.
	QueryOptions
}

// RequestDatacenter returns the datacenter for a given request.
func (q *PreparedQueryExecuteRequest) RequestDatacenter() string {
	return q.Datacenter
}

// CacheInfo implements cache.Request allowing requests to be cached on agent.
func (q *PreparedQueryExecuteRequest) CacheInfo() cache.RequestInfo {
	info := cache.RequestInfo{
		Token:          q.Token,
		Datacenter:     q.Datacenter,
		MinIndex:       q.MinQueryIndex,
		Timeout:        q.MaxQueryTime,
		MaxAge:         q.MaxAge,
		MustRevalidate: q.MustRevalidate,
	}

	// To calculate the cache key we hash over all the fields that affect the
	// output other than Datacenter and Token which are dealt with in the cache
	// framework already. Note the order here is important for the outcome - if we
	// ever care about cache-invalidation on updates e.g. because we persist
	// cached results, we need to be careful we maintain the same order of fields
	// here. We could alternatively use `hash:set` struct tag on an anonymous
	// struct to make it more robust if it becomes significant.
	v, err := hashstructure.Hash([]interface{}{
		q.QueryIDOrName,
		q.Limit,
		q.Connect,
	}, 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
}

// PreparedQueryExecuteRemoteRequest is used when running a local query in a
// remote datacenter.
type PreparedQueryExecuteRemoteRequest struct {
	// Datacenter is the target this request is intended for.
	Datacenter string

	// Query is a copy of the query to execute.  We have to ship the entire
	// query over since it won't be present in the remote state store.
	Query PreparedQuery

	// Limit will trim the resulting list down to the given limit.
	Limit int

	// Connect is the same as ExecuteRequest.
	Connect bool

	// QueryOptions (unfortunately named here) controls the consistency
	// settings for the the service lookups.
	QueryOptions
}

// RequestDatacenter returns the datacenter for a given request.
func (q *PreparedQueryExecuteRemoteRequest) RequestDatacenter() string {
	return q.Datacenter
}

// PreparedQueryExecuteResponse has the results of executing a query.
type PreparedQueryExecuteResponse struct {
	// Service is the service that was queried.
	Service string

	// EnterpriseMeta of the service that was queried.
	acl.EnterpriseMeta

	// Nodes has the nodes that were output by the query.
	Nodes CheckServiceNodes

	// DNS has the options for serving these results over DNS.
	DNS QueryDNSOptions

	// Datacenter is the datacenter that these results came from.
	Datacenter string

	// PeerName specifies the cluster peer that these results came from.
	PeerName string

	// Failovers is a count of how many times we had to query a remote
	// datacenter.
	Failovers int

	// QueryMeta has freshness information about the query.
	QueryMeta
}

// PreparedQueryExplainResponse has the results when explaining a query/
type PreparedQueryExplainResponse struct {
	// Query has the fully-rendered query.
	Query PreparedQuery

	// QueryMeta has freshness information about the query.
	QueryMeta
}
