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

package state

import (
	"fmt"
	"sync"

	"github.com/hashicorp/go-memdb"
	"github.com/hashicorp/nomad/nomad/state/indexer"
	"github.com/hashicorp/nomad/nomad/structs"
)

const (
	tableIndex = "index"

	TableNamespaces           = "namespaces"
	TableNodePools            = "node_pools"
	TableServiceRegistrations = "service_registrations"
	TableVariables            = "variables"
	TableVariablesQuotas      = "variables_quota"
	TableRootKeyMeta          = "root_key_meta"
	TableACLRoles             = "acl_roles"
	TableACLAuthMethods       = "acl_auth_methods"
	TableACLBindingRules      = "acl_binding_rules"
	TableAllocs               = "allocs"
)

const (
	indexID            = "id"
	indexJob           = "job"
	indexNodeID        = "node_id"
	indexAllocID       = "alloc_id"
	indexServiceName   = "service_name"
	indexExpiresGlobal = "expires-global"
	indexExpiresLocal  = "expires-local"
	indexKeyID         = "key_id"
	indexPath          = "path"
	indexName          = "name"
	indexSigningKey    = "signing_key"
	indexAuthMethod    = "auth_method"
)

var (
	schemaFactories SchemaFactories
	factoriesLock   sync.Mutex
)

// SchemaFactory is the factory method for returning a TableSchema
type SchemaFactory func() *memdb.TableSchema
type SchemaFactories []SchemaFactory

// RegisterSchemaFactories is used to register a table schema.
func RegisterSchemaFactories(factories ...SchemaFactory) {
	factoriesLock.Lock()
	defer factoriesLock.Unlock()
	schemaFactories = append(schemaFactories, factories...)
}

func GetFactories() SchemaFactories {
	return schemaFactories
}

func init() {
	// Register all schemas
	RegisterSchemaFactories([]SchemaFactory{
		indexTableSchema,
		nodeTableSchema,
		nodePoolTableSchema,
		jobTableSchema,
		jobSummarySchema,
		jobVersionSchema,
		jobSubmissionSchema,
		deploymentSchema,
		periodicLaunchTableSchema,
		evalTableSchema,
		allocTableSchema,
		vaultAccessorTableSchema,
		siTokenAccessorTableSchema,
		aclPolicyTableSchema,
		aclTokenTableSchema,
		oneTimeTokenTableSchema,
		autopilotConfigTableSchema,
		schedulerConfigTableSchema,
		clusterMetaTableSchema,
		csiVolumeTableSchema,
		csiPluginTableSchema,
		scalingPolicyTableSchema,
		scalingEventTableSchema,
		namespaceTableSchema,
		serviceRegistrationsTableSchema,
		variablesTableSchema,
		variablesQuotasTableSchema,
		variablesRootKeyMetaSchema,
		aclRolesTableSchema,
		aclAuthMethodsTableSchema,
		bindingRulesTableSchema,
	}...)
}

// stateStoreSchema is used to return the schema for the state store
func stateStoreSchema() *memdb.DBSchema {
	// Create the root DB schema
	db := &memdb.DBSchema{
		Tables: make(map[string]*memdb.TableSchema),
	}

	// Add each of the tables
	for _, schemaFn := range GetFactories() {
		schema := schemaFn()
		if _, ok := db.Tables[schema.Name]; ok {
			panic(fmt.Sprintf("duplicate table name: %s", schema.Name))
		}
		db.Tables[schema.Name] = schema
	}
	return db
}

// indexTableSchema is used for tracking the most recent index used for each table.
func indexTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "index",
		Indexes: map[string]*memdb.IndexSchema{
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.StringFieldIndex{
					Field:     "Key",
					Lowercase: true,
				},
			},
		},
	}
}

// nodeTableSchema returns the MemDB schema for the nodes table.
// This table is used to store all the client nodes that are registered.
func nodeTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "nodes",
		Indexes: map[string]*memdb.IndexSchema{
			// Primary index is used for node management
			// and simple direct lookup. ID is required to be
			// unique.
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.UUIDFieldIndex{
					Field: "ID",
				},
			},
			"secret_id": {
				Name:         "secret_id",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.UUIDFieldIndex{
					Field: "SecretID",
				},
			},
			"node_pool": {
				Name:         "node_pool",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "NodePool",
				},
			},
		},
	}
}

// nodePoolTableSchema returns the MemDB schema for the node pools table.
// This table is used to store all the node pools registered in the cluster.
func nodePoolTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: TableNodePools,
		Indexes: map[string]*memdb.IndexSchema{
			// Name is the primary index used for lookup and is required to be
			// unique.
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.StringFieldIndex{
					Field: "Name",
				},
			},
		},
	}
}

// jobTableSchema returns the MemDB schema for the jobs table.
// This table is used to store all the jobs that have been submitted.
func jobTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "jobs",
		Indexes: map[string]*memdb.IndexSchema{
			// Primary index is used for job management
			// and simple direct lookup. ID is required to be
			// unique within a namespace.
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,

				// Use a compound index so the tuple of (Namespace, ID) is
				// uniquely identifying
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},

						&memdb.StringFieldIndex{
							Field: "ID",
						},
					},
				},
			},
			"type": {
				Name:         "type",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field:     "Type",
					Lowercase: false,
				},
			},
			"gc": {
				Name:         "gc",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.ConditionalIndex{
					Conditional: jobIsGCable,
				},
			},
			"periodic": {
				Name:         "periodic",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.ConditionalIndex{
					Conditional: jobIsPeriodic,
				},
			},
			"pool": {
				Name:         "pool",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "NodePool",
				},
			},
		},
	}
}

// jobSummarySchema returns the memdb schema for the job summary table
func jobSummarySchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "job_summary",
		Indexes: map[string]*memdb.IndexSchema{
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,

				// Use a compound index so the tuple of (Namespace, JobID) is
				// uniquely identifying
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},

						&memdb.StringFieldIndex{
							Field: "JobID",
						},
					},
				},
			},
		},
	}
}

// jobVersionSchema returns the memdb schema for the job version table which
// keeps a historical view of job versions.
func jobVersionSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "job_version",
		Indexes: map[string]*memdb.IndexSchema{
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,

				// Use a compound index so the tuple of (Namespace, ID, Version) is
				// uniquely identifying
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},

						&memdb.StringFieldIndex{
							Field:     "ID",
							Lowercase: true,
						},

						&memdb.UintFieldIndex{
							Field: "Version",
						},
					},
				},
			},
		},
	}
}

// jobSubmissionSchema returns the memdb table schema of job submissions
// which contain the original source material of each job, per version.
func jobSubmissionSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "job_submission",
		Indexes: map[string]*memdb.IndexSchema{
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				// index by (Namespace, JobID, Version)
				// note: uniqueness applies only at the moment of insertion,
				// if anything modifies one of these fields (as the stored
				// struct is a pointer, there is no consistency)
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},

						&memdb.StringFieldIndex{
							Field:     "JobID",
							Lowercase: true,
						},

						&memdb.UintFieldIndex{
							Field: "Version",
						},
					},
				},
			},
		},
	}
}

// jobIsGCable satisfies the ConditionalIndexFunc interface and creates an index
// on whether a job is eligible for garbage collection.
func jobIsGCable(obj interface{}) (bool, error) {
	j, ok := obj.(*structs.Job)
	if !ok {
		return false, fmt.Errorf("Unexpected type: %v", obj)
	}

	// If the job is periodic or parameterized it is only garbage collectable if
	// it is stopped.
	periodic := j.Periodic != nil && j.Periodic.Enabled
	parameterized := j.IsParameterized()
	if periodic || parameterized {
		return j.Stop, nil
	}

	// If the job isn't dead it isn't eligible
	if j.Status != structs.JobStatusDead {
		return false, nil
	}

	// Any job that is stopped is eligible for garbage collection
	if j.Stop {
		return true, nil
	}

	switch j.Type {
	// Otherwise, batch and sysbatch jobs are eligible because they complete on
	// their own without a user stopping them.
	case structs.JobTypeBatch, structs.JobTypeSysBatch:
		return true, nil

	default:
		// other job types may not be GC until stopped
		return false, nil
	}
}

// jobIsPeriodic satisfies the ConditionalIndexFunc interface and creates an index
// on whether a job is periodic.
func jobIsPeriodic(obj interface{}) (bool, error) {
	j, ok := obj.(*structs.Job)
	if !ok {
		return false, fmt.Errorf("Unexpected type: %v", obj)
	}

	if j.Periodic != nil && j.Periodic.Enabled {
		return true, nil
	}

	return false, nil
}

// deploymentSchema returns the MemDB schema tracking a job's deployments
func deploymentSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "deployment",
		Indexes: map[string]*memdb.IndexSchema{
			// id index is used for direct lookup of an deployment by ID.
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.UUIDFieldIndex{
					Field: "ID",
				},
			},

			// create index is used for listing deploy, ordering them by
			// creation chronology. (Use a reverse iterator for newest first).
			//
			// There may be more than one deployment per CreateIndex.
			"create": {
				Name:         "create",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.UintFieldIndex{
							Field: "CreateIndex",
						},
						&memdb.StringFieldIndex{
							Field: "ID",
						},
					},
				},
			},

			// namespace is used to lookup evaluations by namespace.
			"namespace": {
				Name:         "namespace",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "Namespace",
				},
			},

			// namespace_create index is used to lookup deployments by namespace
			// in their original chronological order based on CreateIndex.
			//
			// Use a prefix iterator (namespace_create_prefix) to iterate deployments
			// of a Namespace in order of CreateIndex.
			//
			// There may be more than one deployment per CreateIndex.
			"namespace_create": {
				Name:         "namespace_create",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.CompoundIndex{
					AllowMissing: false,
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},
						&memdb.UintFieldIndex{
							Field: "CreateIndex",
						},
						&memdb.StringFieldIndex{
							Field: "ID",
						},
					},
				},
			},

			// job index is used to lookup deployments by job
			"job": {
				Name:         "job",
				AllowMissing: false,
				Unique:       false,

				// Use a compound index so the tuple of (Namespace, JobID) is
				// uniquely identifying
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},

						&memdb.StringFieldIndex{
							Field: "JobID",
						},
					},
				},
			},
		},
	}
}

// periodicLaunchTableSchema returns the MemDB schema tracking the most recent
// launch time for a periodic job.
func periodicLaunchTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "periodic_launch",
		Indexes: map[string]*memdb.IndexSchema{
			// Primary index is used for job management
			// and simple direct lookup. ID is required to be
			// unique.
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,

				// Use a compound index so the tuple of (Namespace, JobID) is
				// uniquely identifying
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},

						&memdb.StringFieldIndex{
							Field: "ID",
						},
					},
				},
			},
		},
	}
}

// evalTableSchema returns the MemDB schema for the eval table.
// This table is used to store all the evaluations that are pending
// or recently completed.
func evalTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "evals",
		Indexes: map[string]*memdb.IndexSchema{
			// id index is used for direct lookup of an evaluation by ID.
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.UUIDFieldIndex{
					Field: "ID",
				},
			},

			// create index is used for listing evaluations, ordering them by
			// creation chronology. (Use a reverse iterator for newest first).
			"create": {
				Name:         "create",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.UintFieldIndex{
							Field: "CreateIndex",
						},
						&memdb.StringFieldIndex{
							Field: "ID",
						},
					},
				},
			},

			// job index is used to lookup evaluations by job ID.
			"job": {
				Name:         "job",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},

						&memdb.StringFieldIndex{
							Field:     "JobID",
							Lowercase: true,
						},

						&memdb.StringFieldIndex{
							Field:     "Status",
							Lowercase: true,
						},
					},
				},
			},

			// namespace is used to lookup evaluations by namespace.
			"namespace": {
				Name:         "namespace",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "Namespace",
				},
			},

			// namespace_create index is used to lookup evaluations by namespace
			// in their original chronological order based on CreateIndex.
			//
			// Use a prefix iterator (namespace_prefix) on a Namespace to iterate
			// those evaluations in order of CreateIndex.
			"namespace_create": {
				Name:         "namespace_create",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.CompoundIndex{
					AllowMissing: false,
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},
						&memdb.UintFieldIndex{
							Field: "CreateIndex",
						},
						&memdb.StringFieldIndex{
							Field: "ID",
						},
					},
				},
			},
		},
	}
}

// allocTableSchema returns the MemDB schema for the allocation table.
// This table is used to store all the task allocations between task groups
// and nodes.
func allocTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "allocs",
		Indexes: map[string]*memdb.IndexSchema{
			// id index is used for direct lookup of allocation by ID.
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.UUIDFieldIndex{
					Field: "ID",
				},
			},

			// create index is used for listing allocations, ordering them by
			// creation chronology. (Use a reverse iterator for newest first).
			"create": {
				Name:         "create",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.UintFieldIndex{
							Field: "CreateIndex",
						},
						&memdb.StringFieldIndex{
							Field: "ID",
						},
					},
				},
			},

			// namespace is used to lookup evaluations by namespace.
			// todo(shoenig): i think we can deprecate this and other like it
			"namespace": {
				Name:         "namespace",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "Namespace",
				},
			},

			// namespace_create index is used to lookup evaluations by namespace
			// in their original chronological order based on CreateIndex.
			//
			// Use a prefix iterator (namespace_prefix) on a Namespace to iterate
			// those evaluations in order of CreateIndex.
			"namespace_create": {
				Name:         "namespace_create",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.CompoundIndex{
					AllowMissing: false,
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},
						&memdb.UintFieldIndex{
							Field: "CreateIndex",
						},
						&memdb.StringFieldIndex{
							Field: "ID",
						},
					},
				},
			},

			// Node index is used to lookup allocations by node
			"node": {
				Name:         "node",
				AllowMissing: true, // Missing is allow for failed allocations
				Unique:       false,
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field:     "NodeID",
							Lowercase: true,
						},

						// Conditional indexer on if allocation is terminal
						&memdb.ConditionalIndex{
							Conditional: func(obj interface{}) (bool, error) {
								// Cast to allocation
								alloc, ok := obj.(*structs.Allocation)
								if !ok {
									return false, fmt.Errorf("wrong type, got %t should be Allocation", obj)
								}

								// Check if the allocation is terminal
								return alloc.TerminalStatus(), nil
							},
						},
					},
				},
			},

			// Job index is used to lookup allocations by job
			"job": {
				Name:         "job",
				AllowMissing: false,
				Unique:       false,

				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},

						&memdb.StringFieldIndex{
							Field: "JobID",
						},
					},
				},
			},

			// Eval index is used to lookup allocations by eval
			"eval": {
				Name:         "eval",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.UUIDFieldIndex{
					Field: "EvalID",
				},
			},

			// Deployment index is used to lookup allocations by deployment
			"deployment": {
				Name:         "deployment",
				AllowMissing: true,
				Unique:       false,
				Indexer: &memdb.UUIDFieldIndex{
					Field: "DeploymentID",
				},
			},

			// signing_key index is used to lookup live allocations by signing
			// key ID
			indexSigningKey: {
				Name:         indexSigningKey,
				AllowMissing: true, // terminal allocations won't be indexed
				Unique:       false,
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "SigningKeyID",
						},
						&memdb.ConditionalIndex{
							Conditional: func(obj interface{}) (bool, error) {
								alloc, ok := obj.(*structs.Allocation)
								if !ok {
									return false, fmt.Errorf(
										"wrong type, got %t should be Allocation", obj)
								}
								// note: this isn't alloc.TerminalStatus(),
								// because we only want to consider the key
								// unused if the allocation is terminal on both
								// server and client
								return !(alloc.ClientTerminalStatus() && alloc.ServerTerminalStatus()), nil
							},
						},
					},
				},
			},
		},
	}
}

// vaultAccessorTableSchema returns the MemDB schema for the Vault Accessor
// Table. This table tracks Vault accessors for tokens created on behalf of
// allocations required Vault tokens.
func vaultAccessorTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "vault_accessors",
		Indexes: map[string]*memdb.IndexSchema{
			// The primary index is the accessor id
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.StringFieldIndex{
					Field: "Accessor",
				},
			},

			"alloc_id": {
				Name:         "alloc_id",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "AllocID",
				},
			},

			"node_id": {
				Name:         "node_id",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "NodeID",
				},
			},
		},
	}
}

// siTokenAccessorTableSchema returns the MemDB schema for the Service Identity
// token accessor table. This table tracks accessors for tokens created on behalf
// of allocations with Consul connect enabled tasks that need SI tokens.
func siTokenAccessorTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: siTokenAccessorTable,
		Indexes: map[string]*memdb.IndexSchema{
			// The primary index is the accessor id
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.StringFieldIndex{
					Field: "AccessorID",
				},
			},

			"alloc_id": {
				Name:         "alloc_id",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "AllocID",
				},
			},

			"node_id": {
				Name:         "node_id",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "NodeID",
				},
			},
		},
	}
}

// aclPolicyTableSchema returns the MemDB schema for the policy table.
// This table is used to store the policies which are referenced by tokens
func aclPolicyTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "acl_policy",
		Indexes: map[string]*memdb.IndexSchema{
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.StringFieldIndex{
					Field: "Name",
				},
			},
			"job": {
				Name:         "job",
				AllowMissing: true,
				Unique:       false,
				Indexer:      &ACLPolicyJobACLFieldIndex{},
			},
		},
	}
}

// ACLPolicyJobACLFieldIndex is used to extract the policy's JobACL field and
// build an index on it.
type ACLPolicyJobACLFieldIndex struct{}

// FromObject is used to extract an index value from an
// object or to indicate that the index value is missing.
func (a *ACLPolicyJobACLFieldIndex) FromObject(obj interface{}) (bool, []byte, error) {
	policy, ok := obj.(*structs.ACLPolicy)
	if !ok {
		return false, nil, fmt.Errorf("object %#v is not an ACLPolicy", obj)
	}

	if policy.JobACL == nil {
		return false, nil, nil
	}

	ns := policy.JobACL.Namespace
	if ns == "" {
		return false, nil, nil
	}
	jobID := policy.JobACL.JobID
	if jobID == "" {
		return false, nil, fmt.Errorf(
			"object %#v is not a valid ACLPolicy: JobACL.JobID without Namespace", obj)
	}

	val := ns + "\x00" + jobID + "\x00"
	return true, []byte(val), nil
}

// FromArgs is used to build an exact index lookup based on arguments
func (a *ACLPolicyJobACLFieldIndex) FromArgs(args ...interface{}) ([]byte, error) {
	if len(args) != 2 {
		return nil, fmt.Errorf("must provide two arguments")
	}
	arg0, ok := args[0].(string)
	if !ok {
		return nil, fmt.Errorf("argument must be a string: %#v", args[0])
	}
	arg1, ok := args[1].(string)
	if !ok {
		return nil, fmt.Errorf("argument must be a string: %#v", args[0])
	}

	// Add the null character as a terminator
	arg0 += "\x00" + arg1 + "\x00"
	return []byte(arg0), nil
}

// PrefixFromArgs returns a prefix that should be used for scanning based on the arguments
func (a *ACLPolicyJobACLFieldIndex) PrefixFromArgs(args ...interface{}) ([]byte, error) {
	val, err := a.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
}

// aclTokenTableSchema returns the MemDB schema for the tokens table.
// This table is used to store the bearer tokens which are used to authenticate
func aclTokenTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "acl_token",
		Indexes: map[string]*memdb.IndexSchema{
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.UUIDFieldIndex{
					Field: "AccessorID",
				},
			},
			"create": {
				Name:         "create",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.UintFieldIndex{
							Field: "CreateIndex",
						},
						&memdb.StringFieldIndex{
							Field: "AccessorID",
						},
					},
				},
			},
			"secret": {
				Name:         "secret",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.UUIDFieldIndex{
					Field: "SecretID",
				},
			},
			"global": {
				Name:         "global",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.FieldSetIndex{
					Field: "Global",
				},
			},
			indexExpiresGlobal: {
				Name:         indexExpiresGlobal,
				AllowMissing: true,
				Unique:       false,
				Indexer: indexer.SingleIndexer{
					ReadIndex:  indexer.ReadIndex(indexer.IndexFromTimeQuery),
					WriteIndex: indexer.WriteIndex(indexExpiresGlobalFromACLToken),
				},
			},
			indexExpiresLocal: {
				Name:         indexExpiresLocal,
				AllowMissing: true,
				Unique:       false,
				Indexer: indexer.SingleIndexer{
					ReadIndex:  indexer.ReadIndex(indexer.IndexFromTimeQuery),
					WriteIndex: indexer.WriteIndex(indexExpiresLocalFromACLToken),
				},
			},
		},
	}
}

func indexExpiresLocalFromACLToken(raw interface{}) ([]byte, error) {
	return indexExpiresFromACLToken(raw, false)
}

func indexExpiresGlobalFromACLToken(raw interface{}) ([]byte, error) {
	return indexExpiresFromACLToken(raw, true)
}

// indexExpiresFromACLToken implements the indexer.WriteIndex interface and
// allows us to use an ACL tokens ExpirationTime as an index, if it is a
// non-default value. This allows for efficient lookups when trying to deal
// with removal of expired tokens from state.
func indexExpiresFromACLToken(raw interface{}, global bool) ([]byte, error) {
	p, ok := raw.(*structs.ACLToken)
	if !ok {
		return nil, fmt.Errorf("unexpected type %T for structs.ACLToken index", raw)
	}
	if p.Global != global {
		return nil, indexer.ErrMissingValueForIndex
	}
	if !p.HasExpirationTime() {
		return nil, indexer.ErrMissingValueForIndex
	}
	if p.ExpirationTime.Unix() < 0 {
		return nil, fmt.Errorf("token expiration time cannot be before the unix epoch: %s", p.ExpirationTime)
	}

	var b indexer.IndexBuilder
	b.Time(*p.ExpirationTime)
	return b.Bytes(), nil
}

// oneTimeTokenTableSchema returns the MemDB schema for the tokens table.
// This table is used to store one-time tokens for ACL tokens
func oneTimeTokenTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "one_time_token",
		Indexes: map[string]*memdb.IndexSchema{
			"secret": {
				Name:         "secret",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.UUIDFieldIndex{
					Field: "OneTimeSecretID",
				},
			},
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.UUIDFieldIndex{
					Field: "AccessorID",
				},
			},
		},
	}
}

// singletonRecord can be used to describe tables which should contain only 1 entry.
// Example uses include storing node config or cluster metadata blobs.
var singletonRecord = &memdb.ConditionalIndex{
	Conditional: func(interface{}) (bool, error) { return true, nil },
}

// schedulerConfigTableSchema returns the MemDB schema for the scheduler config table.
// This table is used to store configuration options for the scheduler
func schedulerConfigTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "scheduler_config",
		Indexes: map[string]*memdb.IndexSchema{
			"id": {
				Name:         "id",
				AllowMissing: true,
				Unique:       true,
				Indexer:      singletonRecord, // we store only 1 scheduler config
			},
		},
	}
}

// clusterMetaTableSchema returns the MemDB schema for the scheduler config table.
func clusterMetaTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "cluster_meta",
		Indexes: map[string]*memdb.IndexSchema{
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				Indexer:      singletonRecord, // we store only 1 cluster metadata
			},
		},
	}
}

// CSIVolumes are identified by id globally, and searchable by driver
func csiVolumeTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "csi_volumes",
		Indexes: map[string]*memdb.IndexSchema{
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},
						&memdb.StringFieldIndex{
							Field: "ID",
						},
					},
				},
			},
			"plugin_id": {
				Name:         "plugin_id",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "PluginID",
				},
			},
		},
	}
}

// CSIPlugins are identified by id globally, and searchable by driver
func csiPluginTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "csi_plugins",
		Indexes: map[string]*memdb.IndexSchema{
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.StringFieldIndex{
					Field: "ID",
				},
			},
		},
	}
}

// ScalingPolicyTargetFieldIndex is used to extract a field from an object
// using reflection and builds an index on that field.
type ScalingPolicyTargetFieldIndex struct {
	Field string

	// AllowMissing controls if the field should be ignored if the field is
	// not provided.
	AllowMissing bool
}

// FromObject is used to extract an index value from an
// object or to indicate that the index value is missing.
func (s *ScalingPolicyTargetFieldIndex) FromObject(obj interface{}) (bool, []byte, error) {
	policy, ok := obj.(*structs.ScalingPolicy)
	if !ok {
		return false, nil, fmt.Errorf("object %#v is not a ScalingPolicy", obj)
	}

	if policy.Target == nil {
		return false, nil, nil
	}

	val, ok := policy.Target[s.Field]
	if !ok && !s.AllowMissing {
		return false, nil, nil
	}

	// Add the null character as a terminator
	val += "\x00"
	return true, []byte(val), nil
}

// FromArgs is used to build an exact index lookup based on arguments
func (s *ScalingPolicyTargetFieldIndex) 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])
	}
	// Add the null character as a terminator
	arg += "\x00"
	return []byte(arg), nil
}

// PrefixFromArgs returns a prefix that should be used for scanning based on the arguments
func (s *ScalingPolicyTargetFieldIndex) PrefixFromArgs(args ...interface{}) ([]byte, error) {
	val, err := s.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
}

// scalingPolicyTableSchema returns the MemDB schema for the policy table.
func scalingPolicyTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "scaling_policy",
		Indexes: map[string]*memdb.IndexSchema{
			// Primary index is used for simple direct lookup.
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,

				// UUID is uniquely identifying
				Indexer: &memdb.StringFieldIndex{
					Field: "ID",
				},
			},
			// Target index is used for listing by namespace or job, or looking up a specific target.
			// A given task group can have only a single scaling policies, so this is guaranteed to be unique.
			"target": {
				Name:   "target",
				Unique: false,

				// Use a compound index so the tuple of (Namespace, Job, Group, Task) is
				// used when looking for a policy
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&ScalingPolicyTargetFieldIndex{
							Field:        "Namespace",
							AllowMissing: true,
						},

						&ScalingPolicyTargetFieldIndex{
							Field:        "Job",
							AllowMissing: true,
						},

						&ScalingPolicyTargetFieldIndex{
							Field:        "Group",
							AllowMissing: true,
						},

						&ScalingPolicyTargetFieldIndex{
							Field:        "Task",
							AllowMissing: true,
						},
					},
				},
			},
			// Type index is used for listing by policy type
			"type": {
				Name:         "type",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "Type",
				},
			},
			// Used to filter by enabled
			"enabled": {
				Name:         "enabled",
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.FieldSetIndex{
					Field: "Enabled",
				},
			},
		},
	}
}

// scalingEventTableSchema returns the memdb schema for job scaling events
func scalingEventTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: "scaling_event",
		Indexes: map[string]*memdb.IndexSchema{
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,

				// Use a compound index so the tuple of (Namespace, JobID) is
				// uniquely identifying
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},

						&memdb.StringFieldIndex{
							Field: "JobID",
						},
					},
				},
			},
		},
	}
}

// namespaceTableSchema returns the MemDB schema for the namespace table.
func namespaceTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: TableNamespaces,
		Indexes: map[string]*memdb.IndexSchema{
			"id": {
				Name:         "id",
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.StringFieldIndex{
					Field: "Name",
				},
			},
			"quota": {
				Name:         "quota",
				AllowMissing: true,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "Quota",
				},
			},
		},
	}
}

// serviceRegistrationsTableSchema returns the MemDB schema for Nomad native
// service registrations.
func serviceRegistrationsTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: TableServiceRegistrations,
		Indexes: map[string]*memdb.IndexSchema{
			// The serviceID in combination with namespace forms a unique
			// identifier for a service registration. This is used to look up
			// and delete services in individual isolation.
			indexID: {
				Name:         indexID,
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},
						&memdb.StringFieldIndex{
							Field: "ID",
						},
					},
				},
			},
			indexServiceName: {
				Name:         indexServiceName,
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},
						&memdb.StringFieldIndex{
							Field: "ServiceName",
						},
					},
				},
			},
			indexJob: {
				Name:         indexJob,
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},
						&memdb.StringFieldIndex{
							Field: "JobID",
						},
					},
				},
			},
			// The nodeID index allows lookups and deletions to be performed
			// for an entire node. This is primarily used when a node becomes
			// lost.
			indexNodeID: {
				Name:         indexNodeID,
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "NodeID",
				},
			},
			indexAllocID: {
				Name:         indexAllocID,
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "AllocID",
				},
			},
		},
	}
}

// variablesTableSchema returns the MemDB schema for Nomad variables.
func variablesTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: TableVariables,
		Indexes: map[string]*memdb.IndexSchema{
			indexID: {
				Name:         indexID,
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.CompoundIndex{
					Indexes: []memdb.Indexer{
						&memdb.StringFieldIndex{
							Field: "Namespace",
						},
						&memdb.StringFieldIndex{
							Field: "Path",
						},
					},
				},
			},
			indexKeyID: {
				Name:         indexKeyID,
				AllowMissing: false,
				Indexer:      &variableKeyIDFieldIndexer{},
			},
			indexPath: {
				Name:         indexPath,
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "Path",
				},
			},
		},
	}
}

type variableKeyIDFieldIndexer struct{}

// FromArgs implements go-memdb/Indexer and is used to build an exact
// index lookup based on arguments
func (s *variableKeyIDFieldIndexer) 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])
	}
	// Add the null character as a terminator
	arg += "\x00"
	return []byte(arg), nil
}

// PrefixFromArgs implements go-memdb/PrefixIndexer and returns a
// prefix that should be used for scanning based on the arguments
func (s *variableKeyIDFieldIndexer) PrefixFromArgs(args ...interface{}) ([]byte, error) {
	val, err := s.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
}

// FromObject implements go-memdb/SingleIndexer and is used to extract
// an index value from an object or to indicate that the index value
// is missing.
func (s *variableKeyIDFieldIndexer) FromObject(obj interface{}) (bool, []byte, error) {
	variable, ok := obj.(*structs.VariableEncrypted)
	if !ok {
		return false, nil, fmt.Errorf("object %#v is not a Variable", obj)
	}

	keyID := variable.KeyID
	if keyID == "" {
		return false, nil, nil
	}

	// Add the null character as a terminator
	keyID += "\x00"
	return true, []byte(keyID), nil
}

// variablesQuotasTableSchema returns the MemDB schema for Nomad variables
// quotas tracking
func variablesQuotasTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: TableVariablesQuotas,
		Indexes: map[string]*memdb.IndexSchema{
			indexID: {
				Name:         indexID,
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.StringFieldIndex{
					Field:     "Namespace",
					Lowercase: true,
				},
			},
		},
	}
}

// variablesRootKeyMetaSchema returns the MemDB schema for Nomad root keys
func variablesRootKeyMetaSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: TableRootKeyMeta,
		Indexes: map[string]*memdb.IndexSchema{
			indexID: {
				Name:         indexID,
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.StringFieldIndex{
					Field:     "KeyID",
					Lowercase: true,
				},
			},
		},
	}
}

func aclRolesTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: TableACLRoles,
		Indexes: map[string]*memdb.IndexSchema{
			indexID: {
				Name:         indexID,
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.StringFieldIndex{
					Field: "ID",
				},
			},
			indexName: {
				Name:         indexName,
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.StringFieldIndex{
					Field: "Name",
				},
			},
		},
	}
}

func aclAuthMethodsTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: TableACLAuthMethods,
		Indexes: map[string]*memdb.IndexSchema{
			indexID: {
				Name:         indexID,
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.StringFieldIndex{
					Field: "Name",
				},
			},
		},
	}
}

func bindingRulesTableSchema() *memdb.TableSchema {
	return &memdb.TableSchema{
		Name: TableACLBindingRules,
		Indexes: map[string]*memdb.IndexSchema{
			indexID: {
				Name:         indexID,
				AllowMissing: false,
				Unique:       true,
				Indexer: &memdb.StringFieldIndex{
					Field: "ID",
				},
			},
			indexAuthMethod: {
				Name:         indexAuthMethod,
				AllowMissing: false,
				Unique:       false,
				Indexer: &memdb.StringFieldIndex{
					Field: "AuthMethod",
				},
			},
		},
	}
}
