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

package state

import (
	"errors"
	"fmt"

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

// UpsertServiceRegistrations is used to insert a number of service
// registrations into the state store. It uses a single write transaction for
// efficiency, however, any error means no entries will be committed.
func (s *StateStore) UpsertServiceRegistrations(
	msgType structs.MessageType, index uint64, services []*structs.ServiceRegistration) error {

	// Grab a write transaction, so we can use this across all service inserts.
	txn := s.db.WriteTxnMsgT(msgType, index)
	defer txn.Abort()

	// updated tracks whether any inserts have been made. This allows us to
	// skip updating the index table if we do not need to.
	var updated bool

	// Iterate the array of services. In the event of a single error, all
	// inserts fail via the txn.Abort() defer.
	for _, service := range services {
		serviceUpdated, err := s.upsertServiceRegistrationTxn(index, txn, service)
		if err != nil {
			return err
		}
		// Ensure we track whether any inserts have been made.
		updated = updated || serviceUpdated
	}

	// If we did not perform any inserts, exit early.
	if !updated {
		return nil
	}

	// Perform the index table update to mark the new inserts.
	if err := txn.Insert(tableIndex, &IndexEntry{TableServiceRegistrations, index}); err != nil {
		return fmt.Errorf("index update failed: %v", err)
	}

	return txn.Commit()
}

// upsertServiceRegistrationTxn inserts a single service registration into the
// state store using the provided write transaction. It is the responsibility
// of the caller to update the index table.
func (s *StateStore) upsertServiceRegistrationTxn(
	index uint64, txn *txn, service *structs.ServiceRegistration) (bool, error) {

	existing, err := txn.First(TableServiceRegistrations, indexID, service.Namespace, service.ID)
	if err != nil {
		return false, fmt.Errorf("service registration lookup failed: %v", err)
	}

	// Set up the indexes correctly to ensure existing indexes are maintained.
	if existing != nil {
		exist := existing.(*structs.ServiceRegistration)
		if exist.Equal(service) {
			return false, nil
		}
		service.CreateIndex = exist.CreateIndex
		service.ModifyIndex = index
	} else {
		service.CreateIndex = index
		service.ModifyIndex = index
	}

	// Insert the service registration into the table.
	if err := txn.Insert(TableServiceRegistrations, service); err != nil {
		return false, fmt.Errorf("service registration insert failed: %v", err)
	}
	return true, nil
}

// DeleteServiceRegistrationByID is responsible for deleting a single service
// registration based on it's ID and namespace. If the service registration is
// not found within state, an error will be returned.
func (s *StateStore) DeleteServiceRegistrationByID(
	msgType structs.MessageType, index uint64, namespace, id string) error {

	txn := s.db.WriteTxnMsgT(msgType, index)
	defer txn.Abort()

	if err := s.deleteServiceRegistrationByIDTxn(index, txn, namespace, id); err != nil {
		return err
	}
	return txn.Commit()
}

func (s *StateStore) deleteServiceRegistrationByIDTxn(
	index uint64, txn *txn, namespace, id string) error {

	// Lookup the service registration by its ID and namespace. This is a
	// unique index and therefore there will be a maximum of one entry.
	existing, err := txn.First(TableServiceRegistrations, indexID, namespace, id)
	if err != nil {
		return fmt.Errorf("service registration lookup failed: %v", err)
	}
	if existing == nil {
		return errors.New("service registration not found")
	}

	// Delete the existing entry from the table.
	if err := txn.Delete(TableServiceRegistrations, existing); err != nil {
		return fmt.Errorf("service registration deletion failed: %v", err)
	}

	// Update the index table to indicate an update has occurred.
	if err := txn.Insert(tableIndex, &IndexEntry{TableServiceRegistrations, index}); err != nil {
		return fmt.Errorf("index update failed: %v", err)
	}
	return nil
}

// DeleteServiceRegistrationByNodeID deletes all service registrations that
// belong on a single node. If there are no registrations tied to the nodeID,
// the call will noop without an error.
func (s *StateStore) DeleteServiceRegistrationByNodeID(
	msgType structs.MessageType, index uint64, nodeID string) error {

	txn := s.db.WriteTxnMsgT(msgType, index)
	defer txn.Abort()

	num, err := txn.DeleteAll(TableServiceRegistrations, indexNodeID, nodeID)
	if err != nil {
		return fmt.Errorf("deleting service registrations failed: %v", err)
	}

	// If we did not delete any entries, do not update the index table.
	// Otherwise, update the table with the latest index.
	switch num {
	case 0:
		return nil
	default:
		if err := txn.Insert(tableIndex, &IndexEntry{TableServiceRegistrations, index}); err != nil {
			return fmt.Errorf("index update failed: %v", err)
		}
	}

	return txn.Commit()
}

// GetServiceRegistrations returns an iterator that contains all service
// registrations stored within state. This is primarily useful when performing
// listings which use the namespace wildcard operator. The caller is
// responsible for ensuring ACL access is confirmed, or filtering is performed
// before responding.
func (s *StateStore) GetServiceRegistrations(ws memdb.WatchSet) (memdb.ResultIterator, error) {
	txn := s.db.ReadTxn()

	// Walk the entire table.
	iter, err := txn.Get(TableServiceRegistrations, indexID)
	if err != nil {
		return nil, fmt.Errorf("service registration lookup failed: %v", err)
	}
	ws.Add(iter.WatchCh())
	return iter, nil
}

// GetServiceRegistrationsByNamespace returns an iterator that contains all
// registrations belonging to the provided namespace.
func (s *StateStore) GetServiceRegistrationsByNamespace(
	ws memdb.WatchSet, namespace string) (memdb.ResultIterator, error) {
	txn := s.db.ReadTxn()

	// Walk the entire table.
	iter, err := txn.Get(TableServiceRegistrations, indexID+"_prefix", namespace, "")
	if err != nil {
		return nil, fmt.Errorf("service registration lookup failed: %v", err)
	}
	ws.Add(iter.WatchCh())

	return iter, nil
}

// GetServiceRegistrationByName returns an iterator that contains all service
// registrations whose namespace and name match the input parameters. This func
// therefore represents how to identify a single, collection of services that
// are logically grouped together.
func (s *StateStore) GetServiceRegistrationByName(
	ws memdb.WatchSet, namespace, name string) (memdb.ResultIterator, error) {

	txn := s.db.ReadTxn()

	iter, err := txn.Get(TableServiceRegistrations, indexServiceName, namespace, name)
	if err != nil {
		return nil, fmt.Errorf("service registration lookup failed: %v", err)
	}
	ws.Add(iter.WatchCh())

	return iter, nil
}

// GetServiceRegistrationByID returns a single registration. The registration
// will be nil, if no matching entry was found; it is the responsibility of the
// caller to check for this.
func (s *StateStore) GetServiceRegistrationByID(
	ws memdb.WatchSet, namespace, id string) (*structs.ServiceRegistration, error) {

	txn := s.db.ReadTxn()

	watchCh, existing, err := txn.FirstWatch(TableServiceRegistrations, indexID, namespace, id)
	if err != nil {
		return nil, fmt.Errorf("service registration lookup failed: %v", err)
	}
	ws.Add(watchCh)

	if existing != nil {
		return existing.(*structs.ServiceRegistration), nil
	}
	return nil, nil
}

// GetServiceRegistrationsByAllocID returns an iterator containing all the
// service registrations corresponding to a single allocation.
func (s *StateStore) GetServiceRegistrationsByAllocID(
	ws memdb.WatchSet, allocID string) (memdb.ResultIterator, error) {

	txn := s.db.ReadTxn()

	iter, err := txn.Get(TableServiceRegistrations, indexAllocID, allocID)
	if err != nil {
		return nil, fmt.Errorf("service registration lookup failed: %v", err)
	}
	ws.Add(iter.WatchCh())

	return iter, nil
}

// GetServiceRegistrationsByJobID returns an iterator containing all the
// service registrations corresponding to a single job.
func (s *StateStore) GetServiceRegistrationsByJobID(
	ws memdb.WatchSet, namespace, jobID string) (memdb.ResultIterator, error) {

	txn := s.db.ReadTxn()

	iter, err := txn.Get(TableServiceRegistrations, indexJob, namespace, jobID)
	if err != nil {
		return nil, fmt.Errorf("service registration lookup failed: %v", err)
	}
	ws.Add(iter.WatchCh())

	return iter, nil
}

// GetServiceRegistrationsByNodeID identifies all service registrations tied to
// the specified nodeID. This is useful for performing an in-memory lookup in
// order to avoid calling DeleteServiceRegistrationByNodeID via a Raft message.
func (s *StateStore) GetServiceRegistrationsByNodeID(
	ws memdb.WatchSet, nodeID string) ([]*structs.ServiceRegistration, error) {

	txn := s.db.ReadTxn()

	iter, err := txn.Get(TableServiceRegistrations, indexNodeID, nodeID)
	if err != nil {
		return nil, fmt.Errorf("service registration lookup failed: %v", err)
	}
	ws.Add(iter.WatchCh())

	var result []*structs.ServiceRegistration
	for raw := iter.Next(); raw != nil; raw = iter.Next() {
		result = append(result, raw.(*structs.ServiceRegistration))
	}

	return result, nil
}
