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

package inmem

import (
	"context"
	"sync"
	"time"

	"github.com/hashicorp/go-memdb"

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

// Store implements an in-memory resource database using go-memdb.
//
// It can be used as a storage backend directly via the Backend type in this
// package, but also handles reads in our Raft backend, and can be used as a
// local cache when storing data in external systems (e.g. RDBMS, K/V stores).
type Store struct {
	mu sync.RWMutex // guards db, because Restore.Commit will replace it wholesale.
	db *memdb.MemDB

	pub *stream.EventPublisher

	// eventLock is used to serialize operations that result in the publishing of
	// events (i.e. writes and deletes) to ensure correct ordering when there are
	// concurrent writers.
	//
	// We cannot rely on MemDB's write lock for this, because events must be
	// published *after* the transaction is committed to provide monotonic reads
	// between Watch and Read calls. In other words, if we were to publish an event
	// before the transaction was committed, there would be a small window of time
	// where a watcher (e.g. controller) could try to Read the resource and not get
	// the version they were notified about.
	//
	// Without this lock, it would be possible to publish events out-of-order.
	eventLock sync.Mutex
}

// NewStore creates a Store.
//
// You must call Run before using the store.
func NewStore() (*Store, error) {
	db, err := newDB()
	if err != nil {
		return nil, err
	}

	s := &Store{
		db:  db,
		pub: stream.NewEventPublisher(10 * time.Second),
	}
	s.pub.RegisterHandler(eventTopic, s.watchSnapshot, false)

	return s, nil
}

// Run until the given context is canceled. This method blocks, so should be
// called in a goroutine.
func (s *Store) Run(ctx context.Context) { s.pub.Run(ctx) }

// Read a resource using its ID.
//
// For more information, see the storage.Backend documentation.
func (s *Store) Read(id *pbresource.ID) (*pbresource.Resource, error) {
	tx := s.txn(false)

	defer tx.Abort()

	val, err := tx.First(tableNameResources, indexNameID, id)
	if err != nil {
		return nil, err
	}
	if val == nil {
		return nil, storage.ErrNotFound
	}

	res := val.(*pbresource.Resource)

	// Observe the Uid if it was given.
	if id.Uid != "" && res.Id.Uid != id.Uid {
		return nil, storage.ErrNotFound
	}

	// Let the caller know they need to upgrade/downgrade the schema version.
	if id.Type.GroupVersion != res.Id.Type.GroupVersion {
		return nil, storage.GroupVersionMismatchError{
			RequestedType: id.Type,
			Stored:        res,
		}
	}

	return res, nil
}

// WriteCAS performs an atomic Compare-And-Swap (CAS) write of a resource.
//
// For more information, see the storage.Backend documentation.
func (s *Store) WriteCAS(res *pbresource.Resource, vsn string) error {
	s.eventLock.Lock()
	defer s.eventLock.Unlock()

	tx := s.txn(true)
	defer tx.Abort()

	existing, err := tx.First(tableNameResources, indexNameID, res.Id)
	if err != nil {
		return err
	}

	// Callers provide an empty version string on initial resource creation.
	if existing == nil && vsn != "" {
		return storage.ErrCASFailure
	}

	if existing != nil {
		existingRes := existing.(*pbresource.Resource)

		// Uid is immutable.
		if existingRes.Id.Uid != res.Id.Uid {
			return storage.ErrWrongUid
		}

		// Ensure CAS semantics.
		if existingRes.Version != vsn {
			return storage.ErrCASFailure
		}
	}

	if err := tx.Insert(tableNameResources, res); err != nil {
		return err
	}

	idx, err := incrementEventIndex(tx)
	if err != nil {
		return nil
	}
	tx.Commit()

	s.publishEvent(idx, pbresource.WatchEvent_OPERATION_UPSERT, res)

	return nil
}

// DeleteCAS performs an atomic Compare-And-Swap (CAS) deletion of a resource.
//
// For more information, see the storage.Backend documentation.
func (s *Store) DeleteCAS(id *pbresource.ID, vsn string) error {
	s.eventLock.Lock()
	defer s.eventLock.Unlock()

	tx := s.txn(true)
	defer tx.Abort()

	existing, err := tx.First(tableNameResources, indexNameID, id)
	if err != nil {
		return err
	}

	// Deleting an already deleted resource is a no-op.
	if existing == nil {
		return nil
	}

	res := existing.(*pbresource.Resource)

	// Deleting a resource using a previous Uid is a no-op.
	if id.Uid != res.Id.Uid {
		return nil
	}

	// Ensure CAS semantics.
	if vsn != res.Version {
		return storage.ErrCASFailure
	}

	if err := tx.Delete(tableNameResources, id); err != nil {
		return err
	}

	idx, err := incrementEventIndex(tx)
	if err != nil {
		return nil
	}
	tx.Commit()

	s.publishEvent(idx, pbresource.WatchEvent_OPERATION_DELETE, res)

	return nil
}

// List resources of the given type, tenancy, and optionally matching the given
// name prefix.
//
// For more information, see the storage.Backend documentation.
func (s *Store) List(typ storage.UnversionedType, ten *pbresource.Tenancy, namePrefix string) ([]*pbresource.Resource, error) {
	tx := s.txn(false)
	defer tx.Abort()

	return listTxn(tx, query{typ, ten, namePrefix})
}

func listTxn(tx *memdb.Txn, q query) ([]*pbresource.Resource, error) {
	iter, err := tx.Get(tableNameResources, indexNameID+"_prefix", q)
	if err != nil {
		return nil, err
	}

	list := make([]*pbresource.Resource, 0)
	for v := iter.Next(); v != nil; v = iter.Next() {
		res := v.(*pbresource.Resource)

		if q.matches(res) {
			list = append(list, res)
		}
	}
	return list, nil
}

// WatchList watches resources of the given type, tenancy, and optionally
// matching the given name prefix.
//
// For more information, see the storage.Backend documentation.
func (s *Store) WatchList(typ storage.UnversionedType, ten *pbresource.Tenancy, namePrefix string) (*Watch, error) {
	// If the user specifies a wildcard, we subscribe to events for resources in
	// all partitions, peers, and namespaces, and manually filter out irrelevant
	// stuff (in Watch.Next).
	//
	// If the user gave exact tenancy values, we can subscribe to events for the
	// relevant resources only, which is far more efficient.
	var sub stream.Subject
	if ten.Partition == storage.Wildcard ||
		ten.PeerName == storage.Wildcard ||
		ten.Namespace == storage.Wildcard {
		sub = wildcardSubject{typ}
	} else {
		sub = tenancySubject{typ, ten}
	}

	ss, err := s.pub.Subscribe(&stream.SubscribeRequest{
		Topic:   eventTopic,
		Subject: sub,
	})
	if err != nil {
		return nil, err
	}

	return &Watch{
		sub: ss,
		query: query{
			resourceType: typ,
			tenancy:      ten,
			namePrefix:   namePrefix,
		},
	}, nil
}

// ListByOwner returns resources owned by the resource with the given ID.
//
// For more information, see the storage.Backend documentation.
func (s *Store) ListByOwner(id *pbresource.ID) ([]*pbresource.Resource, error) {
	tx := s.txn(false)
	defer tx.Abort()

	iter, err := tx.Get(tableNameResources, indexNameOwner, id)
	if err != nil {
		return nil, err
	}

	var res []*pbresource.Resource
	for v := iter.Next(); v != nil; v = iter.Next() {
		res = append(res, v.(*pbresource.Resource))
	}
	return res, nil
}

func (s *Store) txn(write bool) *memdb.Txn {
	s.mu.RLock()
	defer s.mu.RUnlock()

	return s.db.Txn(write)
}
