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

package state

import (
	"fmt"

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

// nodePoolInit creates the built-in node pools that should always be present
// in the cluster.
func (s *StateStore) nodePoolInit() error {
	allNodePool := &structs.NodePool{
		Name:        structs.NodePoolAll,
		Description: structs.NodePoolAllDescription,
	}

	defaultNodePool := &structs.NodePool{
		Name:        structs.NodePoolDefault,
		Description: structs.NodePoolDefaultDescription,
	}

	return s.UpsertNodePools(
		structs.SystemInitializationType,
		1,
		[]*structs.NodePool{allNodePool, defaultNodePool},
	)
}

// NodePools returns an iterator over all node pools.
func (s *StateStore) NodePools(ws memdb.WatchSet, sort SortOption) (memdb.ResultIterator, error) {
	txn := s.db.ReadTxn()

	var iter memdb.ResultIterator
	var err error

	switch sort {
	case SortReverse:
		iter, err = txn.GetReverse(TableNodePools, "id")
	default:
		iter, err = txn.Get(TableNodePools, "id")
	}
	if err != nil {
		return nil, fmt.Errorf("node pools lookup failed: %w", err)
	}

	ws.Add(iter.WatchCh())
	return iter, nil
}

// NodePoolByName returns the node pool that matches the given name or nil if
// there is no match.
func (s *StateStore) NodePoolByName(ws memdb.WatchSet, name string) (*structs.NodePool, error) {
	txn := s.db.ReadTxn()
	return s.nodePoolByNameTxn(txn, ws, name)
}

func (s *StateStore) nodePoolByNameTxn(txn *txn, ws memdb.WatchSet, name string) (*structs.NodePool, error) {
	watchCh, existing, err := txn.FirstWatch(TableNodePools, "id", name)
	if err != nil {
		return nil, fmt.Errorf("node pool lookup failed: %w", err)
	}
	ws.Add(watchCh)

	if existing == nil {
		return nil, nil
	}

	return existing.(*structs.NodePool), nil
}

// NodePoolsByNamePrefix returns an interator over all node pools that match
// the given name prefix.
func (s *StateStore) NodePoolsByNamePrefix(ws memdb.WatchSet, namePrefix string, sort SortOption) (memdb.ResultIterator, error) {
	txn := s.db.ReadTxn()

	var iter memdb.ResultIterator
	var err error

	switch sort {
	case SortReverse:
		iter, err = txn.GetReverse(TableNodePools, "id_prefix", namePrefix)
	default:
		iter, err = txn.Get(TableNodePools, "id_prefix", namePrefix)
	}
	if err != nil {
		return nil, fmt.Errorf("node pools prefix lookup failed: %w", err)
	}

	ws.Add(iter.WatchCh())
	return iter, nil
}

// nodePoolExists returs true if a node pool with the give name exists.
func (s *StateStore) nodePoolExists(txn *txn, pool string) (bool, error) {
	existing, err := txn.First(TableNodePools, "id", pool)
	return existing != nil, err
}

// UpsertNodePools inserts or updates the given set of node pools.
func (s *StateStore) UpsertNodePools(msgType structs.MessageType, index uint64, pools []*structs.NodePool) error {
	txn := s.db.WriteTxnMsgT(msgType, index)
	defer txn.Abort()

	for _, pool := range pools {
		if err := s.upsertNodePoolTxn(txn, index, pool); err != nil {
			return err
		}
	}

	if err := txn.Insert("index", &IndexEntry{TableNodePools, index}); err != nil {
		return fmt.Errorf("index update failed: %w", err)
	}

	return txn.Commit()
}

func (s *StateStore) upsertNodePoolTxn(txn *txn, index uint64, pool *structs.NodePool) error {
	if pool == nil {
		return nil
	}

	existing, err := txn.First(TableNodePools, "id", pool.Name)
	if err != nil {
		return fmt.Errorf("node pool lookup failed: %w", err)
	}

	if existing != nil {
		// Prevent changes to built-in node pools.
		if pool.IsBuiltIn() {
			return fmt.Errorf("modifying node pool %q is not allowed", pool.Name)
		}

		exist := existing.(*structs.NodePool)
		pool.CreateIndex = exist.CreateIndex
		pool.ModifyIndex = index
	} else {
		pool.CreateIndex = index
		pool.ModifyIndex = index
	}

	if err := txn.Insert(TableNodePools, pool); err != nil {
		return fmt.Errorf("node pool insert failed: %w", err)
	}

	return nil
}

// fetchOrCreateNodePoolTxn returns an existing node pool with the given name
// or creates a new one if it doesn't exist.
func (s *StateStore) fetchOrCreateNodePoolTxn(txn *txn, index uint64, name string) (*structs.NodePool, error) {
	pool, err := s.nodePoolByNameTxn(txn, nil, name)
	if err != nil {
		return nil, err
	}

	if pool == nil {
		pool = &structs.NodePool{Name: name}
		err = s.upsertNodePoolTxn(txn, index, pool)
		if err != nil {
			return nil, err
		}
	}

	return pool, nil
}

// DeleteNodePools removes the given set of node pools.
func (s *StateStore) DeleteNodePools(msgType structs.MessageType, index uint64, names []string) error {
	txn := s.db.WriteTxnMsgT(msgType, index)
	defer txn.Abort()

	for _, n := range names {
		if err := s.deleteNodePoolTxn(txn, index, n); err != nil {
			return err
		}
	}

	// Update index table.
	if err := txn.Insert("index", &IndexEntry{TableNodePools, index}); err != nil {
		return fmt.Errorf("index update failed: %w", err)
	}

	return txn.Commit()
}

func (s *StateStore) deleteNodePoolTxn(txn *txn, index uint64, name string) error {
	// Check if node pool exists.
	existing, err := txn.First(TableNodePools, "id", name)
	if err != nil {
		return fmt.Errorf("node pool lookup failed: %w", err)
	}
	if existing == nil {
		return fmt.Errorf("node pool %s not found", name)
	}

	pool := existing.(*structs.NodePool)

	// Prevent deletion of built-in node pools.
	if pool.IsBuiltIn() {
		return fmt.Errorf("deleting node pool %q is not allowed", pool.Name)
	}

	// Delete node pool.
	if err := txn.Delete(TableNodePools, pool); err != nil {
		return fmt.Errorf("node pool deletion failed: %w", err)
	}

	return nil
}
