// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package inmem

import (
	"bytes"
	"fmt"
	"strings"

	"github.com/hashicorp/go-memdb"

	"github.com/hashicorp/consul/internal/storage"
	"github.com/hashicorp/consul/proto-public/pbresource"
)

const (
	tableNameMetadata  = "metadata"
	tableNameResources = "resources"

	indexNameID    = "id"
	indexNameOwner = "owner"

	metaKeyEventIndex = "index"
)

func newDB() (*memdb.MemDB, error) {
	return memdb.NewMemDB(&memdb.DBSchema{
		Tables: map[string]*memdb.TableSchema{
			tableNameMetadata: {
				Name: tableNameMetadata,
				Indexes: map[string]*memdb.IndexSchema{
					indexNameID: {
						Name:         indexNameID,
						AllowMissing: false,
						Unique:       true,
						Indexer:      &memdb.StringFieldIndex{Field: "Key"},
					},
				},
			},
			tableNameResources: {
				Name: tableNameResources,
				Indexes: map[string]*memdb.IndexSchema{
					indexNameID: {
						Name:         indexNameID,
						AllowMissing: false,
						Unique:       true,
						Indexer:      idIndexer{},
					},
					indexNameOwner: {
						Name:         indexNameOwner,
						AllowMissing: true,
						Unique:       false,
						Indexer:      ownerIndexer{},
					},
				},
			},
		},
	})
}

// indexSeparator delimits the segments of our radix tree keys.
const indexSeparator = "\x00"

// idIndexer implements the memdb.Indexer, memdb.SingleIndexer and
// memdb.PrefixIndexer interfaces. It is used for indexing resources
// by their IDs.
type idIndexer struct{}

// FromArgs constructs a radix tree key from an ID for lookup.
func (i idIndexer) FromArgs(args ...any) ([]byte, error) {
	if l := len(args); l != 1 {
		return nil, fmt.Errorf("expected 1 arg, got: %d", l)
	}
	id, ok := args[0].(*pbresource.ID)
	if !ok {
		return nil, fmt.Errorf("expected *pbresource.ID, got: %T", args[0])
	}
	return indexFromID(id, false), nil
}

// FromObject constructs a radix tree key from a Resource at write-time, or an
// ID at delete-time.
func (i idIndexer) FromObject(raw any) (bool, []byte, error) {
	switch t := raw.(type) {
	case *pbresource.ID:
		return true, indexFromID(t, false), nil
	case *pbresource.Resource:
		return true, indexFromID(t.Id, false), nil
	}
	return false, nil, fmt.Errorf("expected *pbresource.Resource or *pbresource.ID, got: %T", raw)
}

// PrefixFromArgs constructs a radix tree key prefix from a query for listing.
func (i idIndexer) PrefixFromArgs(args ...any) ([]byte, error) {
	if l := len(args); l != 1 {
		return nil, fmt.Errorf("expected 1 arg, got: %d", l)
	}

	q, ok := args[0].(query)
	if !ok {
		return nil, fmt.Errorf("expected query, got: %T", args[0])
	}
	return q.indexPrefix(), nil
}

// ownerIndexer implements the memdb.Indexer and memdb.SingleIndexer interfaces.
// It is used for indexing resources by their owners.
type ownerIndexer struct{}

// FromArgs constructs a radix tree key from an ID for lookup.
func (i ownerIndexer) FromArgs(args ...any) ([]byte, error) {
	if l := len(args); l != 1 {
		return nil, fmt.Errorf("expected 1 arg, got: %d", l)
	}
	id, ok := args[0].(*pbresource.ID)
	if !ok {
		return nil, fmt.Errorf("expected *pbresource.ID, got: %T", args[0])
	}
	return indexFromID(id, true), nil
}

// FromObject constructs a radix key tree from a Resource at write-time.
func (i ownerIndexer) FromObject(raw any) (bool, []byte, error) {
	res, ok := raw.(*pbresource.Resource)
	if !ok {
		return false, nil, fmt.Errorf("expected *pbresource.Resource, got: %T", raw)
	}
	if res.Owner == nil {
		return false, nil, nil
	}
	return true, indexFromID(res.Owner, true), nil
}

func indexFromType(t storage.UnversionedType) []byte {
	var b indexBuilder
	b.String(t.Group)
	b.String(t.Kind)
	return b.Bytes()
}

func indexFromTenancy(t *pbresource.Tenancy) []byte {
	var b indexBuilder
	b.String(t.Partition)
	b.String(t.Namespace)
	return b.Bytes()
}

func indexFromID(id *pbresource.ID, includeUid bool) []byte {
	var b indexBuilder
	b.Raw(indexFromType(storage.UnversionedTypeFrom(id.Type)))
	b.Raw(indexFromTenancy(id.Tenancy))
	// TODO(peering/v2) handle peer tenancy for indexing
	b.String(id.Name)
	if includeUid {
		b.String(id.Uid)
	}
	return b.Bytes()
}

type indexBuilder bytes.Buffer

func (i *indexBuilder) Raw(v []byte) {
	(*bytes.Buffer)(i).Write(v)
}

func (i *indexBuilder) String(s string) {
	(*bytes.Buffer)(i).WriteString(s)
	(*bytes.Buffer)(i).WriteString(indexSeparator)
}

func (i *indexBuilder) Bytes() []byte {
	return (*bytes.Buffer)(i).Bytes()
}

type query struct {
	resourceType storage.UnversionedType
	tenancy      *pbresource.Tenancy
	namePrefix   string
}

// indexPrefix is called by idIndexer.PrefixFromArgs to construct a radix tree
// key prefix for list queries.
//
// Our radix tree keys are structured like so:
//
//	<type><partition><namespace><name>
//
// Where each segment is followed by a NULL terminator.
//
// In order to handle wildcard queries, we return a prefix up to the wildcarded
// field. For example:
//
//	 Query: type={mesh,v1,service}, partition=default, namespace=default
//	Prefix: mesh[NULL]v1[NULL]service[NULL]default[NULL]
//
// Which means that we must manually apply filters after the wildcard (i.e.
// namespace in the above example) in the matches method.
func (q query) indexPrefix() []byte {
	var b indexBuilder
	b.Raw(indexFromType(q.resourceType))

	if v := q.tenancy.Partition; v == storage.Wildcard {
		return b.Bytes()
	} else {
		b.String(v)
	}

	if v := q.tenancy.Namespace; v == storage.Wildcard {
		return b.Bytes()
	} else {
		b.String(v)
	}

	// TODO(peering/v2) handle peer tenancies

	if q.namePrefix != "" {
		b.Raw([]byte(q.namePrefix))
	}

	return b.Bytes()
}

// matches applies filters that couldn't be applied by just doing a radix tree
// prefix scan, because an earlier segment of the key prefix was wildcarded.
//
// See docs on query.indexPrefix for an example.
func (q query) matches(res *pbresource.Resource) bool {
	if q.tenancy.Partition != storage.Wildcard && res.Id.Tenancy.Partition != q.tenancy.Partition {
		return false
	}

	if q.tenancy.Namespace != storage.Wildcard && res.Id.Tenancy.Namespace != q.tenancy.Namespace {
		return false
	}

	// TODO(peering/v2) handle peer tenancies

	if len(q.namePrefix) != 0 && !strings.HasPrefix(res.Id.Name, q.namePrefix) {
		return false
	}

	return true
}
