// 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"
	"golang.org/x/exp/slices"
)

// ACLTokensByExpired returns an array accessor IDs of expired ACL tokens.
// Their expiration is determined against the passed time.Time value.
//
// The function handles global and local tokens independently as determined by
// the global boolean argument. The number of returned IDs can be limited by
// the max integer, which is useful to limit the number of tokens we attempt to
// delete in a single transaction.
func (s *StateStore) ACLTokensByExpired(global bool) (memdb.ResultIterator, error) {
	tnx := s.db.ReadTxn()

	iter, err := tnx.Get("acl_token", expiresIndexName(global))
	if err != nil {
		return nil, fmt.Errorf("failed acl token listing: %v", err)
	}
	return iter, nil
}

// expiresIndexName is a helper function to identify the correct ACL token
// table expiry index to use.
func expiresIndexName(global bool) string {
	if global {
		return indexExpiresGlobal
	}
	return indexExpiresLocal
}

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

	// Grab a write transaction.
	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 roles. In the event of a single error, all inserts
	// fail via the txn.Abort() defer.
	for _, role := range roles {

		roleUpdated, err := s.upsertACLRoleTxn(index, txn, role, allowMissingPolicies)
		if err != nil {
			return err
		}

		// Ensure we track whether any inserts have been made.
		updated = updated || roleUpdated
	}

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

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

	return txn.Commit()
}

// upsertACLRoleTxn inserts a single ACL role into the state store using the
// provided write transaction. It is the responsibility of the caller to update
// the index table.
func (s *StateStore) upsertACLRoleTxn(
	index uint64, txn *txn, role *structs.ACLRole, allowMissingPolicies bool) (bool, error) {

	// Ensure the role hash is not zero to provide defense in depth. This
	// should be done outside the state store, so we do not spend time here
	// and thus Raft, when it, can be avoided.
	if len(role.Hash) == 0 {
		role.SetHash()
	}

	// This validation also happens within the RPC handler, but Raft latency
	// could mean that by the time the state call is invoked, another Raft
	// update has deleted policies detailed in role. Therefore, check again
	// while in our write txn.
	if !allowMissingPolicies {
		if err := s.validateACLRolePolicyLinksTxn(txn, role); err != nil {
			return false, err
		}
	}

	// This validation also happens within the RPC handler, but Raft latency
	// could mean that by the time the state call is invoked, another Raft
	// update has already written a role with the same name. We therefore need
	// to check we are not trying to create a role with an existing name.
	existingRaw, err := txn.First(TableACLRoles, indexName, role.Name)
	if err != nil {
		return false, fmt.Errorf("ACL role lookup failed: %v", err)
	}

	// Track our type asserted role, so we only need to do this once.
	var existing *structs.ACLRole

	// If we did not find an ACL Role within state with the same name, we need
	// to check using the ID index as the operator might be performing an
	// update on the role name.
	//
	// If we found an entry using the name index, we need to check that the ID
	// matches the object within the request.
	if existingRaw == nil {
		existingRaw, err = txn.First(TableACLRoles, indexID, role.ID)
		if err != nil {
			return false, fmt.Errorf("ACL role lookup failed: %v", err)
		}
		if existingRaw != nil {
			existing = existingRaw.(*structs.ACLRole)
		}
	} else {
		existing = existingRaw.(*structs.ACLRole)
		if existing.ID != role.ID {
			return false, fmt.Errorf("ACL role with name %s already exists", role.Name)
		}
	}

	// Depending on whether this is an initial create, or an update, we need to
	// check and set certain parameters. The most important is to ensure any
	// create index is carried over.
	if existing != nil {

		// If the role already exists, check whether the update contains any
		// difference. If it doesn't, we can avoid a state update as wel as
		// updates to any blocking queries.
		if existing.Equal(role) {
			return false, nil
		}

		role.CreateIndex = existing.CreateIndex
		role.ModifyIndex = index
	} else {
		role.CreateIndex = index
		role.ModifyIndex = index
	}

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

// validateACLRolePolicyLinksTxn is the same as ValidateACLRolePolicyLinks but
// allows callers to pass their own transaction.
func (s *StateStore) validateACLRolePolicyLinksTxn(txn *txn, role *structs.ACLRole) error {
	for _, policyLink := range role.Policies {
		_, existing, err := txn.FirstWatch("acl_policy", indexID, policyLink.Name)
		if err != nil {
			return fmt.Errorf("ACL policy lookup failed: %v", err)
		}
		if existing == nil {
			return errors.New("ACL policy not found")
		}
	}
	return nil
}

// DeleteACLRolesByID is responsible for batch deleting ACL roles based on
// their ID. It uses a single write transaction for efficiency, however, any
// error means no entries will be committed. An error is produced if a role is
// not found within state which has been passed within the array.
func (s *StateStore) DeleteACLRolesByID(
	msgType structs.MessageType, index uint64, roleIDs []string) error {

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

	for _, roleID := range roleIDs {
		if err := s.deleteACLRoleByIDTxn(txn, roleID); err != nil {
			return err
		}
	}

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

	return txn.Commit()
}

// deleteACLRoleByIDTxn deletes a single ACL role from the state store using the
// provided write transaction. It is the responsibility of the caller to update
// the index table.
func (s *StateStore) deleteACLRoleByIDTxn(txn *txn, roleID string) error {

	existing, err := txn.First(TableACLRoles, indexID, roleID)
	if err != nil {
		return fmt.Errorf("ACL role lookup failed: %v", err)
	}
	if existing == nil {
		return errors.New("ACL role not found")
	}

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

// GetACLRoles returns an iterator that contains all ACL roles stored within
// state.
func (s *StateStore) GetACLRoles(ws memdb.WatchSet) (memdb.ResultIterator, error) {
	txn := s.db.ReadTxn()

	// Walk the entire table to get all ACL roles.
	iter, err := txn.Get(TableACLRoles, indexID)
	if err != nil {
		return nil, fmt.Errorf("ACL role lookup failed: %v", err)
	}
	ws.Add(iter.WatchCh())

	return iter, nil
}

// GetACLRoleByID returns a single ACL role specified by the input ID. The role
// object will be nil, if no matching entry was found; it is the responsibility
// of the caller to check for this.
func (s *StateStore) GetACLRoleByID(ws memdb.WatchSet, roleID string) (*structs.ACLRole, error) {
	txn := s.db.ReadTxn()
	return s.getACLRoleByIDTxn(txn, ws, roleID)
}

// getACLRoleByIDTxn allows callers to pass a read transaction in order to read
// a single ACL role specified by the input ID. The role object will be nil, if
// no matching entry was found; it is the responsibility of the caller to check
// for this.
func (s *StateStore) getACLRoleByIDTxn(txn ReadTxn, ws memdb.WatchSet, roleID string) (*structs.ACLRole, error) {

	// Perform the ACL role lookup using the "id" index.
	watchCh, existing, err := txn.FirstWatch(TableACLRoles, indexID, roleID)
	if err != nil {
		return nil, fmt.Errorf("ACL role lookup failed: %v", err)
	}
	ws.Add(watchCh)

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

// GetACLRoleByName returns a single ACL role specified by the input name. The
// role object will be nil, if no matching entry was found; it is the
// responsibility of the caller to check for this.
func (s *StateStore) GetACLRoleByName(ws memdb.WatchSet, roleName string) (*structs.ACLRole, error) {
	txn := s.db.ReadTxn()

	// Perform the ACL role lookup using the "name" index.
	watchCh, existing, err := txn.FirstWatch(TableACLRoles, indexName, roleName)
	if err != nil {
		return nil, fmt.Errorf("ACL role lookup failed: %v", err)
	}
	ws.Add(watchCh)

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

// GetACLRoleByIDPrefix is used to lookup ACL policies using a prefix to match
// on the ID.
func (s *StateStore) GetACLRoleByIDPrefix(ws memdb.WatchSet, idPrefix string) (memdb.ResultIterator, error) {
	txn := s.db.ReadTxn()

	iter, err := txn.Get(TableACLRoles, indexID+"_prefix", idPrefix)
	if err != nil {
		return nil, fmt.Errorf("ACL role lookup failed: %v", err)
	}
	ws.Add(iter.WatchCh())

	return iter, nil
}

// fixTokenRoleLinks is a state helper that ensures the returned ACL token has
// an accurate representation of ACL role links. The role links could have
// become stale when a linked role was deleted or renamed. This will correct
// them and generates a newly allocated token only when fixes are needed. If
// the role links are still accurate, we just return the original token.
func (s *StateStore) fixTokenRoleLinks(txn ReadTxn, original *structs.ACLToken) (*structs.ACLToken, error) {

	// Track whether we have made an initial copy to ensure we are not
	// operating on the token directly from state.
	copied := false

	token := original

	// copyTokenFn is a helper function which copies the ACL token along with
	// a certain number of ACL role links.
	copyTokenFn := func(t *structs.ACLToken, numLinks int) *structs.ACLToken {
		clone := t.Copy()
		clone.Roles = slices.Clone(t.Roles[:numLinks])
		return clone
	}

	for linkIndex, link := range original.Roles {

		// This should never happen, but guard against it anyway, so we log an
		// error rather than panic.
		if link.ID == "" {
			return nil, errors.New("detected corrupted token within the state store: missing role link ID")
		}

		role, err := s.getACLRoleByIDTxn(txn, nil, link.ID)
		if err != nil {
			return nil, err
		}

		if role == nil {
			if !copied {
				// clone the token as we cannot touch the original
				token = copyTokenFn(original, linkIndex)
				copied = true
			}
			// if already owned then we just don't append it.
		} else if role.Name != link.Name {
			if !copied {
				token = copyTokenFn(original, linkIndex)
				copied = true
			}

			// append the corrected policy
			token.Roles = append(token.Roles, &structs.ACLTokenRoleLink{ID: link.ID, Name: role.Name})

		} else if copied {
			token.Roles = append(token.Roles, link)
		}
	}

	return token, nil
}
