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

package state

import (
	"fmt"
	"net"
	"reflect"
	"strings"

	"github.com/hashicorp/go-memdb"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/agent/structs"
)

const (
	tableNodes             = "nodes"
	tableServices          = "services"
	tableChecks            = "checks"
	tableGatewayServices   = "gateway-services"
	tableMeshTopology      = "mesh-topology"
	tableServiceVirtualIPs = "service-virtual-ips"
	tableFreeVirtualIPs    = "free-virtual-ips"
	tableKindServiceNames  = "kind-service-names"

	indexID          = "id"
	indexService     = "service"
	indexConnect     = "connect"
	indexKind        = "kind"
	indexKindOnly    = "kind-only"
	indexStatus      = "status"
	indexNodeService = "node_service"
	indexNode        = "node"
	indexUpstream    = "upstream"
	indexDownstream  = "downstream"
	indexGateway     = "gateway"
	indexUUID        = "uuid"
	indexMeta        = "meta"
	indexCounterOnly = "counter"
	indexManualVIPs  = "manual-vips"
)

// nodesTableSchema returns a new table schema used for storing struct.Node.
func nodesTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: tableNodes,
		Indexes: map[string]*memdb.IndexSchema{
			indexID: {
				Name:         indexID,
				AllowMissing: false,
				Unique:       true,
				Indexer: indexerSingleWithPrefix[Query, *structs.Node, any]{
					readIndex:   indexWithPeerName(indexFromQuery),
					writeIndex:  indexWithPeerName(indexFromNode),
					prefixIndex: prefixIndexFromQueryWithPeer,
				},
			},
			indexUUID: {
				Name:         indexUUID,
				AllowMissing: true,
				Unique:       true,
				Indexer: indexerSingleWithPrefix[Query, *structs.Node, Query]{
					readIndex:   indexWithPeerName(indexFromUUIDQuery),
					writeIndex:  indexWithPeerName(indexIDFromNode),
					prefixIndex: prefixIndexFromUUIDWithPeerQuery,
				},
			},
			indexMeta: {
				Name:         indexMeta,
				AllowMissing: true,
				Unique:       false,
				Indexer: indexerMulti[KeyValueQuery, *structs.Node]{
					readIndex:       indexWithPeerName(indexFromKeyValueQuery),
					writeIndexMulti: multiIndexWithPeerName(indexMetaFromNode),
				},
			},
		},
	}
}

func indexFromNode(n *structs.Node) ([]byte, error) {
	if n.Node == "" {
		return nil, errMissingValueForIndex
	}

	var b indexBuilder
	b.String(strings.ToLower(n.Node))
	return b.Bytes(), nil
}

func indexIDFromNode(n *structs.Node) ([]byte, error) {
	if n.ID == "" {
		return nil, errMissingValueForIndex
	}

	v, err := uuidStringToBytes(string(n.ID))
	if err != nil {
		return nil, err
	}

	return v, nil
}

func indexMetaFromNode(n *structs.Node) ([][]byte, error) {
	// NOTE: this is case-sensitive!

	vals := make([][]byte, 0, len(n.Meta))
	for key, val := range n.Meta {
		if key == "" {
			continue
		}

		var b indexBuilder
		b.String(key)
		b.String(val)
		vals = append(vals, b.Bytes())
	}
	if len(vals) == 0 {
		return nil, errMissingValueForIndex
	}

	return vals, nil
}

// servicesTableSchema returns a new table schema used to store information
// about services.
func servicesTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: tableServices,
		Indexes: map[string]*memdb.IndexSchema{
			indexID: {
				Name:         indexID,
				AllowMissing: false,
				Unique:       true,
				Indexer: indexerSingleWithPrefix[NodeServiceQuery, *structs.ServiceNode, any]{
					readIndex:   indexWithPeerName(indexFromNodeServiceQuery),
					writeIndex:  indexWithPeerName(indexFromServiceNode),
					prefixIndex: prefixIndexFromQueryWithPeer,
				},
			},
			indexNode: {
				Name:         indexNode,
				AllowMissing: false,
				Unique:       false,
				Indexer: indexerSingle[Query, nodeIdentifier]{
					readIndex:  indexWithPeerName(indexFromQuery),
					writeIndex: indexWithPeerName(indexFromNodeIdentity),
				},
			},
			indexService: {
				Name:         indexService,
				AllowMissing: true,
				Unique:       false,
				Indexer: indexerSingle[Query, *structs.ServiceNode]{
					readIndex:  indexWithPeerName(indexFromQuery),
					writeIndex: indexWithPeerName(indexServiceNameFromServiceNode),
				},
			},
			indexConnect: {
				Name:         indexConnect,
				AllowMissing: true,
				Unique:       false,
				Indexer: indexerSingle[Query, *structs.ServiceNode]{
					readIndex:  indexWithPeerName(indexFromQuery),
					writeIndex: indexWithPeerName(indexConnectNameFromServiceNode),
				},
			},
			indexKind: {
				Name:         indexKind,
				AllowMissing: false,
				Unique:       false,
				Indexer: indexerSingle[Query, *structs.ServiceNode]{
					readIndex:  indexWithPeerName(indexFromQuery),
					writeIndex: indexWithPeerName(indexKindFromServiceNode),
				},
			},
		},
	}
}

func indexFromNodeServiceQuery(q NodeServiceQuery) ([]byte, error) {
	var b indexBuilder
	b.String(strings.ToLower(q.Node))
	b.String(strings.ToLower(q.Service))
	return b.Bytes(), nil
}

func indexFromServiceNode(n *structs.ServiceNode) ([]byte, error) {
	if n.Node == "" {
		return nil, errMissingValueForIndex
	}

	var b indexBuilder
	b.String(strings.ToLower(n.Node))
	b.String(strings.ToLower(n.ServiceID))
	return b.Bytes(), nil
}

type nodeIdentifier interface {
	partitionIndexable
	peerIndexable

	NodeIdentity() structs.Identity
}

var _ nodeIdentifier = (*structs.HealthCheck)(nil)
var _ nodeIdentifier = (*structs.ServiceNode)(nil)

func indexFromNodeIdentity(n nodeIdentifier) ([]byte, error) {
	id := n.NodeIdentity()
	if id.ID == "" {
		return nil, errMissingValueForIndex
	}

	var b indexBuilder
	b.String(strings.ToLower(id.ID))
	return b.Bytes(), nil
}

func indexServiceNameFromServiceNode(n *structs.ServiceNode) ([]byte, error) {
	if n.Node == "" {
		return nil, errMissingValueForIndex
	}

	var b indexBuilder
	b.String(strings.ToLower(n.ServiceName))
	return b.Bytes(), nil
}

func indexConnectNameFromServiceNode(n *structs.ServiceNode) ([]byte, error) {
	name, ok := connectNameFromServiceNode(n)
	if !ok {
		return nil, errMissingValueForIndex
	}

	var b indexBuilder
	b.String(strings.ToLower(name))
	return b.Bytes(), nil
}

func connectNameFromServiceNode(sn *structs.ServiceNode) (string, bool) {
	switch {
	case sn.ServiceKind == structs.ServiceKindConnectProxy:
		// For proxies, this service supports Connect for the destination
		return sn.ServiceProxy.DestinationServiceName, true

	case sn.ServiceConnect.Native:
		// For native, this service supports Connect directly
		return sn.ServiceName, true

	default:
		// Doesn't support Connect at all
		return "", false
	}
}

func indexKindFromServiceNode(n *structs.ServiceNode) ([]byte, error) {
	var b indexBuilder
	b.String(strings.ToLower(string(n.ServiceKind)))
	return b.Bytes(), nil
}

// indexWithPeerName adds peer name to the index.
func indexWithPeerName[T peerIndexable](
	fn func(T) ([]byte, error),
) func(T) ([]byte, error) {
	return func(e T) ([]byte, error) {
		v, err := fn(e)
		if err != nil {
			return nil, err
		}

		peername := e.PeerOrEmpty()
		if peername == "" {
			peername = structs.LocalPeerKeyword
		}
		b := newIndexBuilder(len(v) + len(peername) + 1)
		b.String(strings.ToLower(peername))
		b.Raw(v)
		return b.Bytes(), nil
	}
}

// multiIndexWithPeerName adds peer name to multiple indices, and returns multiple indices.
func multiIndexWithPeerName[T any](
	fn func(T) ([][]byte, error),
) func(T) ([][]byte, error) {
	return func(raw T) ([][]byte, error) {
		n, ok := any(raw).(peerIndexable)
		if !ok {
			return nil, fmt.Errorf("type must be peerIndexable: %T", raw)
		}

		results, err := fn(raw)
		if err != nil {
			return nil, err
		}

		peername := n.PeerOrEmpty()
		if peername == "" {
			peername = structs.LocalPeerKeyword
		}
		for i, v := range results {
			b := newIndexBuilder(len(v) + len(peername) + 1)
			b.String(strings.ToLower(peername))
			b.Raw(v)
			results[i] = b.Bytes()
		}
		return results, nil
	}
}

// checksTableSchema returns a new table schema used for storing and indexing
// health check information. Health checks have a number of different attributes
// we want to filter by, so this table is a bit more complex.
func checksTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: tableChecks,
		Indexes: map[string]*memdb.IndexSchema{
			indexID: {
				Name:         indexID,
				AllowMissing: false,
				Unique:       true,
				Indexer: indexerSingleWithPrefix[NodeCheckQuery, *structs.HealthCheck, any]{
					readIndex:   indexWithPeerName(indexFromNodeCheckQuery),
					writeIndex:  indexWithPeerName(indexFromHealthCheck),
					prefixIndex: prefixIndexFromQueryWithPeer,
				},
			},
			indexStatus: {
				Name:         indexStatus,
				AllowMissing: false,
				Unique:       false,
				Indexer: indexerSingle[Query, *structs.HealthCheck]{
					readIndex:  indexWithPeerName(indexFromQuery),
					writeIndex: indexWithPeerName(indexStatusFromHealthCheck),
				},
			},
			indexService: {
				Name:         indexService,
				AllowMissing: true,
				Unique:       false,
				Indexer: indexerSingle[Query, *structs.HealthCheck]{
					readIndex:  indexWithPeerName(indexFromQuery),
					writeIndex: indexWithPeerName(indexServiceNameFromHealthCheck),
				},
			},
			indexNode: {
				Name:         indexNode,
				AllowMissing: true,
				Unique:       false,
				Indexer: indexerSingle[Query, nodeIdentifier]{
					readIndex:  indexWithPeerName(indexFromQuery),
					writeIndex: indexWithPeerName(indexFromNodeIdentity),
				},
			},
			indexNodeService: {
				Name:         indexNodeService,
				AllowMissing: true,
				Unique:       false,
				Indexer: indexerSingle[NodeServiceQuery, *structs.HealthCheck]{
					readIndex:  indexWithPeerName(indexFromNodeServiceQuery),
					writeIndex: indexWithPeerName(indexNodeServiceFromHealthCheck),
				},
			},
		},
	}
}

func indexFromNodeCheckQuery(q NodeCheckQuery) ([]byte, error) {
	if q.Node == "" || q.CheckID == "" {
		return nil, errMissingValueForIndex
	}

	var b indexBuilder
	b.String(strings.ToLower(q.Node))
	b.String(strings.ToLower(q.CheckID))
	return b.Bytes(), nil
}

func indexFromHealthCheck(hc *structs.HealthCheck) ([]byte, error) {
	if hc.Node == "" || hc.CheckID == "" {
		return nil, errMissingValueForIndex
	}

	var b indexBuilder
	b.String(strings.ToLower(hc.Node))
	b.String(strings.ToLower(string(hc.CheckID)))
	return b.Bytes(), nil
}

func indexNodeServiceFromHealthCheck(hc *structs.HealthCheck) ([]byte, error) {
	if hc.Node == "" {
		return nil, errMissingValueForIndex
	}

	var b indexBuilder
	b.String(strings.ToLower(hc.Node))
	b.String(strings.ToLower(hc.ServiceID))
	return b.Bytes(), nil
}

func indexStatusFromHealthCheck(hc *structs.HealthCheck) ([]byte, error) {
	if hc.Status == "" {
		return nil, errMissingValueForIndex
	}

	var b indexBuilder
	b.String(strings.ToLower(hc.Status))
	return b.Bytes(), nil
}

func indexServiceNameFromHealthCheck(hc *structs.HealthCheck) ([]byte, error) {
	if hc.ServiceName == "" {
		return nil, errMissingValueForIndex
	}

	var b indexBuilder
	b.String(strings.ToLower(hc.ServiceName))
	return b.Bytes(), nil
}

// gatewayServicesTableSchema returns a new table schema used to store information
// about services associated with terminating gateways.
func gatewayServicesTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: tableGatewayServices,
		Indexes: map[string]*memdb.IndexSchema{
			indexID: {
				Name:         indexID,
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&ServiceNameIndex{
							Field: "Gateway",
						},
						&ServiceNameIndex{
							Field: "Service",
						},
						&memdb.IntFieldIndex{
							Field: "Port",
						},
					},
				},
			},
			indexGateway: {
				Name:         indexGateway,
				AllowMissing: false,
				Unique:       false,
				Indexer: &ServiceNameIndex{
					Field: "Gateway",
				},
			},
			indexService: {
				Name:         indexService,
				AllowMissing: true,
				Unique:       false,
				Indexer: &ServiceNameIndex{
					Field: "Service",
				},
			},
		},
	}
}

// meshTopologyTableSchema returns a new table schema used to store information
// relating upstream and downstream services
func meshTopologyTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: tableMeshTopology,
		Indexes: map[string]*memdb.IndexSchema{
			indexID: {
				Name:         indexID,
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&ServiceNameIndex{
							Field: "Upstream",
						},
						&ServiceNameIndex{
							Field: "Downstream",
						},
					},
				},
			},
			indexUpstream: {
				Name:         indexUpstream,
				AllowMissing: true,
				Unique:       false,
				Indexer: &ServiceNameIndex{
					Field: "Upstream",
				},
			},
			indexDownstream: {
				Name:         indexDownstream,
				AllowMissing: false,
				Unique:       false,
				Indexer: &ServiceNameIndex{
					Field: "Downstream",
				},
			},
		},
	}
}

type ServiceNameIndex struct {
	Field string
}

func (index *ServiceNameIndex) FromObject(obj interface{}) (bool, []byte, error) {
	v := reflect.ValueOf(obj)
	v = reflect.Indirect(v) // Dereference the pointer if any

	fv := v.FieldByName(index.Field)
	isPtr := fv.Kind() == reflect.Ptr
	fv = reflect.Indirect(fv)
	if !isPtr && !fv.IsValid() || !fv.CanInterface() {
		return false, nil,
			fmt.Errorf("field '%s' for %#v is invalid %v ", index.Field, obj, isPtr)
	}

	name, ok := fv.Interface().(structs.ServiceName)
	if !ok {
		return false, nil, fmt.Errorf("Field 'ServiceName' is not of type structs.ServiceName")
	}

	// Enforce lowercase and add null character as terminator
	id := strings.ToLower(name.String()) + "\x00"

	return true, []byte(id), nil
}

func (index *ServiceNameIndex) FromArgs(args ...interface{}) ([]byte, error) {
	if len(args) != 1 {
		return nil, fmt.Errorf("must provide only a single argument")
	}
	name, ok := args[0].(structs.ServiceName)
	if !ok {
		return nil, fmt.Errorf("argument must be of type structs.ServiceName: %#v", args[0])
	}

	// Enforce lowercase and add null character as terminator
	id := strings.ToLower(name.String()) + "\x00"

	return []byte(strings.ToLower(id)), nil
}

func (index *ServiceNameIndex) PrefixFromArgs(args ...interface{}) ([]byte, error) {
	val, err := index.FromArgs(args...)
	if err != nil {
		return nil, err
	}

	// Strip the null terminator, the rest is a prefix
	n := len(val)
	if n > 0 {
		return val[:n-1], nil
	}
	return val, nil
}

// upstreamDownstream pairs come from individual proxy registrations, which can be updated independently.
type upstreamDownstream struct {
	Upstream   structs.ServiceName
	Downstream structs.ServiceName

	// Refs stores the registrations that contain this pairing.
	// When there are no remaining Refs, the upstreamDownstream can be deleted.
	//
	// Note: This map must be treated as immutable when accessed in MemDB.
	//       The entire upstreamDownstream structure must be deep copied on updates.
	Refs map[string]struct{}

	structs.RaftIndex
}

// NodeCheckQuery is used to query the ID index of the checks table.
type NodeCheckQuery struct {
	Node     string
	CheckID  string
	PeerName string
	acl.EnterpriseMeta
}

type peerIndexable interface {
	PeerOrEmpty() string
}

func (q NodeCheckQuery) PeerOrEmpty() string {
	return q.PeerName
}

// NamespaceOrDefault exists because structs.EnterpriseMeta uses a pointer
// receiver for this method. Remove once that is fixed.
func (q NodeCheckQuery) NamespaceOrDefault() string {
	return q.EnterpriseMeta.NamespaceOrDefault()
}

// PartitionOrDefault exists because structs.EnterpriseMeta uses a pointer
// receiver for this method. Remove once that is fixed.
func (q NodeCheckQuery) PartitionOrDefault() string {
	return q.EnterpriseMeta.PartitionOrDefault()
}

// ServiceVirtualIP is used to store a virtual IP associated with a service.
// It is also used to store assigned virtual IPs when a snapshot is created.
type ServiceVirtualIP struct {
	Service   structs.PeeredServiceName
	IP        net.IP
	ManualIPs []string

	structs.RaftIndex
}

func (s ServiceVirtualIP) IPWithOffset() (string, error) {
	result, err := addIPOffset(startingVirtualIP, s.IP)
	if err != nil {
		return "", err
	}
	return result.String(), nil
}

// FreeVirtualIP is used to store a virtual IP freed up by a service deregistration.
// It is also used to store free virtual IPs when a snapshot is created.
type FreeVirtualIP struct {
	IP        net.IP
	IsCounter bool
}

func counterIndex(obj interface{}) (bool, error) {
	if vip, ok := obj.(FreeVirtualIP); ok {
		return vip.IsCounter, nil
	}
	return false, fmt.Errorf("object is not a virtual IP entry")
}

type ServiceManualVIPIndex struct{}

func (index *ServiceManualVIPIndex) FromObject(obj interface{}) (bool, []byte, error) {
	entry, ok := obj.(ServiceVirtualIP)
	if !ok {
		return false, nil, fmt.Errorf("object is not a ServiceVirtualIP")
	}

	// Enforce lowercase and add null character as terminator
	id := strings.ToLower(entry.Service.ServiceName.PartitionOrDefault()) + "\x00"

	return true, []byte(id), nil
}

func (index *ServiceManualVIPIndex) FromArgs(args ...interface{}) ([]byte, error) {
	if len(args) != 1 {
		return nil, fmt.Errorf("must provide only a single argument")
	}
	arg, ok := args[0].(string)
	if !ok {
		return nil, fmt.Errorf("argument must be a string: %#v", args[0])
	}

	id := strings.ToLower(arg) + "\x00"
	return []byte(id), nil
}

func serviceVirtualIPTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: tableServiceVirtualIPs,
		Indexes: map[string]*memdb.IndexSchema{
			indexID: {
				Name:         indexID,
				AllowMissing: false,
				Unique:       true,
				Indexer: indexerSingleWithPrefix[structs.PeeredServiceName, ServiceVirtualIP, Query]{
					readIndex:  indexFromPeeredServiceName,
					writeIndex: indexFromServiceVirtualIP,
					// Read all peers in a cluster / partition
					prefixIndex: prefixIndexFromQueryWithPeerWildcardable,
				},
			},
			indexManualVIPs: {
				Name:         indexManualVIPs,
				AllowMissing: true,
				Unique:       false,
				Indexer: &memdb.CompoundMultiIndex{
					Indexes: []memdb.Indexer{
						&ServiceManualVIPIndex{},
						&memdb.StringSliceFieldIndex{
							Field: "ManualIPs",
						},
					},
				},
			},
		},
	}
}

func indexFromServiceVirtualIP(vip ServiceVirtualIP) ([]byte, error) {
	if vip.Service.ServiceName.Name == "" {
		return nil, errMissingValueForIndex
	}
	return indexFromPeeredServiceName(vip.Service)
}

func freeVirtualIPTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: tableFreeVirtualIPs,
		Indexes: map[string]*memdb.IndexSchema{
			indexID: {
				Name:         indexID,
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "IP",
						},
						&memdb.ConditionalIndex{
							Conditional: counterIndex,
						},
					},
				},
			},
			indexCounterOnly: {
				Name:         indexCounterOnly,
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.ConditionalIndex{
					Conditional: counterIndex,
				},
			},
		},
	}
}

type KindServiceName struct {
	Kind    structs.ServiceKind
	Service structs.ServiceName

	structs.RaftIndex
}

func (n *KindServiceName) PartitionOrDefault() string {
	return n.Service.PartitionOrDefault()
}

func (n *KindServiceName) NamespaceOrDefault() string {
	return n.Service.NamespaceOrDefault()
}

func kindServiceNameTableSchema() *memdb.TableSchema {
	// TODO(peering): make this peer-aware
	return &memdb.TableSchema{
		Name: tableKindServiceNames,
		Indexes: map[string]*memdb.IndexSchema{
			indexID: {
				Name:         indexID,
				AllowMissing: false,
				Unique:       true,
				Indexer: indexerSingle[any, any]{
					readIndex:  indexFromKindServiceName,
					writeIndex: indexFromKindServiceName,
				},
			},
			indexKind: {
				Name:         indexKind,
				AllowMissing: false,
				Unique:       false,
				Indexer: indexerSingle[enterpriseIndexable, enterpriseIndexable]{
					readIndex:  indexFromKindServiceNameKindOnly,
					writeIndex: indexFromKindServiceNameKindOnly,
				},
			},
		},
	}
}

// KindServiceNameQuery is used to lookup service names by kind or enterprise meta.
type KindServiceNameQuery struct {
	Kind structs.ServiceKind
	Name string
	acl.EnterpriseMeta
}

// NamespaceOrDefault exists because structs.EnterpriseMeta uses a pointer
// receiver for this method. Remove once that is fixed.
func (q KindServiceNameQuery) NamespaceOrDefault() string {
	return q.EnterpriseMeta.NamespaceOrDefault()
}

// PartitionOrDefault exists because structs.EnterpriseMeta uses a pointer
// receiver for this method. Remove once that is fixed.
func (q KindServiceNameQuery) PartitionOrDefault() string {
	return q.EnterpriseMeta.PartitionOrDefault()
}

func indexFromKindServiceNameKindOnly(raw enterpriseIndexable) ([]byte, error) {
	switch x := raw.(type) {
	case *KindServiceName:
		var b indexBuilder
		b.String(strings.ToLower(string(x.Kind)))
		return b.Bytes(), nil

	case Query:
		var b indexBuilder
		b.String(strings.ToLower(x.Value))
		return b.Bytes(), nil

	default:
		return nil, fmt.Errorf("type must be *KindServiceName or Query: %T", raw)
	}
}

func kindServiceNamesMaxIndex(tx ReadTxn, ws memdb.WatchSet, kind string) uint64 {
	return maxIndexWatchTxn(tx, ws, kindServiceNameIndexName(kind))
}

func kindServiceNameIndexName(kind string) string {
	return "kind_service_names." + kind
}
