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

package api

// 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
}

// Deprecated: use QueryFailoverOptions instead.
type QueryDatacenterOptions = QueryFailoverOptions

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

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

	// Partition specifies a partition to try during failover
	// Note: Partition are available only in Consul Enterprise
	Partition string `json:",omitempty"`

	// Namespace specifies a namespace to try during failover
	// Note: Namespaces are available only in Consul Enterprise
	Namespace string `json:",omitempty"`
}

// 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 `json:",omitempty"`

	// Namespace of the service to query
	Namespace string `json:",omitempty"`

	// Partition of the service to query
	Partition string `json:",omitempty"`

	// Near allows baking in the name of a node to automatically distance-
	// sort from. The magic "_agent" value is supported, which sorts near
	// the agent which initiated the request by default.
	Near string

	// Failover controls what we do if there are no healthy nodes in the
	// local datacenter.
	Failover QueryFailoverOptions `json:",omitempty"`

	// 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 []string

	// 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

	// 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
}

// QueryTemplate carries the arguments for creating a templated query.
type QueryTemplate struct {
	// Type specifies the type of the query template. Currently only
	// "name_prefix_match" is supported. This field is required.
	Type string

	// Regexp allows specifying a regex pattern to match against the name
	// of the query being executed.
	Regexp string

	// RemoveEmptyTags if set to true, will cause the Tags list inside
	// the Service structure to be stripped of any empty strings. This is useful
	// when interpolating into tags in a way where the tag is optional, and
	// where searching for an empty tag would yield no results from the query.
	RemoveEmptyTags bool
}

// PreparedQueryDefinition defines a complete prepared query.
type PreparedQueryDefinition 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

	// 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

	// Template is used to pass through the arguments for creating a
	// prepared query with an attached template. If a template is given,
	// interpolations are possible in other struct fields.
	Template QueryTemplate
}

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

	// Namespace of the service that was queried
	Namespace string `json:",omitempty"`

	// Nodes has the nodes that were output by the query.
	Nodes []ServiceEntry

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

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

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

// PreparedQuery can be used to query the prepared query endpoints.
type PreparedQuery struct {
	c *Client
}

// PreparedQuery returns a handle to the prepared query endpoints.
func (c *Client) PreparedQuery() *PreparedQuery {
	return &PreparedQuery{c}
}

// Create makes a new prepared query. The ID of the new query is returned.
func (c *PreparedQuery) Create(query *PreparedQueryDefinition, q *WriteOptions) (string, *WriteMeta, error) {
	r := c.c.newRequest("POST", "/v1/query")
	r.setWriteOptions(q)
	r.obj = query
	rtt, resp, err := c.c.doRequest(r)
	if err != nil {
		return "", nil, err
	}
	defer closeResponseBody(resp)
	if err := requireOK(resp); err != nil {
		return "", nil, err
	}

	wm := &WriteMeta{}
	wm.RequestTime = rtt

	var out struct{ ID string }
	if err := decodeBody(resp, &out); err != nil {
		return "", nil, err
	}
	return out.ID, wm, nil
}

// Update makes updates to an existing prepared query.
func (c *PreparedQuery) Update(query *PreparedQueryDefinition, q *WriteOptions) (*WriteMeta, error) {
	return c.c.write("/v1/query/"+query.ID, query, nil, q)
}

// List is used to fetch all the prepared queries (always requires a management
// token).
func (c *PreparedQuery) List(q *QueryOptions) ([]*PreparedQueryDefinition, *QueryMeta, error) {
	var out []*PreparedQueryDefinition
	qm, err := c.c.query("/v1/query", &out, q)
	if err != nil {
		return nil, nil, err
	}
	return out, qm, nil
}

// Get is used to fetch a specific prepared query.
func (c *PreparedQuery) Get(queryID string, q *QueryOptions) ([]*PreparedQueryDefinition, *QueryMeta, error) {
	var out []*PreparedQueryDefinition
	qm, err := c.c.query("/v1/query/"+queryID, &out, q)
	if err != nil {
		return nil, nil, err
	}
	return out, qm, nil
}

// Delete is used to delete a specific prepared query.
func (c *PreparedQuery) Delete(queryID string, q *WriteOptions) (*WriteMeta, error) {
	r := c.c.newRequest("DELETE", "/v1/query/"+queryID)
	r.setWriteOptions(q)
	rtt, resp, err := c.c.doRequest(r)
	if err != nil {
		return nil, err
	}
	defer closeResponseBody(resp)
	if err := requireOK(resp); err != nil {
		return nil, err
	}

	wm := &WriteMeta{}
	wm.RequestTime = rtt
	return wm, nil
}

// Execute is used to execute a specific prepared query. You can execute using
// a query ID or name.
func (c *PreparedQuery) Execute(queryIDOrName string, q *QueryOptions) (*PreparedQueryExecuteResponse, *QueryMeta, error) {
	var out *PreparedQueryExecuteResponse
	qm, err := c.c.query("/v1/query/"+queryIDOrName+"/execute", &out, q)
	if err != nil {
		return nil, nil, err
	}
	return out, qm, nil
}
