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

package token

import (
	"sync"

	"crypto/subtle"
)

type TokenSource bool

const (
	TokenSourceConfig TokenSource = false
	TokenSourceAPI    TokenSource = true
)

type TokenKind int

const (
	TokenKindAgent TokenKind = iota
	TokenKindAgentRecovery
	TokenKindUser
	TokenKindReplication
	TokenKindConfigFileRegistration
	TokenKindDNS
)

type watcher struct {
	kind TokenKind
	ch   chan<- struct{}
}

// Notifier holds the channel used to notify a watcher
// of token updates as well as some internal tracking
// information to allow for deregistering the notifier.
type Notifier struct {
	id int
	Ch <-chan struct{}
}

// Store is used to hold the special ACL tokens used by Consul agents. It is
// designed to update the tokens on the fly, so the token store itself should be
// plumbed around and used to get tokens at runtime, don't save the resulting
// tokens.
type Store struct {
	// l synchronizes access to the token store.
	l sync.RWMutex

	// userToken is passed along for requests when the user didn't supply a
	// token, and may be left blank to use the anonymous token. This will
	// also be used for agent operations if the agent token isn't set.
	userToken string

	// userTokenSource indicates where this token originated from.
	userTokenSource TokenSource

	// agentToken is used for internal agent operations like self-registering
	// with the catalog and anti-entropy, but should never be used for
	// user-initiated operations.
	agentToken string

	// agentTokenSource indicates where this token originated from.
	agentTokenSource TokenSource

	// agentRecoveryToken is a special token that's only used locally for
	// access to the /v1/agent utility operations if the servers aren't
	// available.
	agentRecoveryToken string

	// agentRecoveryTokenSource indicates where this token originated from.
	agentRecoveryTokenSource TokenSource

	// replicationToken is a special token that's used by servers to
	// replicate data from the primary datacenter.
	replicationToken string

	// replicationTokenSource indicates where this token originated from.
	replicationTokenSource TokenSource

	// configFileRegistrationToken is used to register services and checks
	// that are defined in configuration files.
	configFileRegistrationToken string

	// configFileRegistrationTokenSource indicates where this token originated from.
	configFileRegistrationTokenSource TokenSource

	// dnsToken is a special token that is used as the implicit token for DNS requests
	// as well as for DNS-specific RPC requests.
	dnsToken string

	// dnsTokenSource indicates where the dnsToken originated from.
	dnsTokenSource TokenSource

	watchers     map[int]watcher
	watcherIndex int

	persistence *fileStore
	// persistenceLock is used to synchronize access to the persisted token store
	// within the data directory. This will prevent loading while writing as well as
	// multiple concurrent writes.
	persistenceLock sync.RWMutex

	// enterpriseTokens contains tokens only used in consul-enterprise
	enterpriseTokens
}

// Notify will set up a watch for when tokens of the desired kind is changed
func (t *Store) Notify(kind TokenKind) Notifier {
	// buffering ensures that notifications aren't missed if the watcher
	// isn't already in a select and that our notifications don't
	// block returning from the Update* methods.
	ch := make(chan struct{}, 1)

	w := watcher{
		kind: kind,
		ch:   ch,
	}

	t.l.Lock()
	defer t.l.Unlock()
	if t.watchers == nil {
		t.watchers = make(map[int]watcher)
	}
	// we specifically want to avoid the zero-value to prevent accidental stop-notification requests
	t.watcherIndex += 1
	t.watchers[t.watcherIndex] = w

	return Notifier{id: t.watcherIndex, Ch: ch}
}

// StopNotify stops the token store from sending notifications to the specified notifiers chan
func (t *Store) StopNotify(n Notifier) {
	t.l.Lock()
	defer t.l.Unlock()
	delete(t.watchers, n.id)
}

// anyKindAllowed returns true if any of the kinds in the `check` list are
// set to be allowed in the `allowed` map.
//
// Note: this is mostly just a convenience to simplify the code in
// sendNotificationLocked and prevent more nested looping with breaks/continues
// and other state tracking.
func anyKindAllowed(allowed TokenKind, check []TokenKind) bool {
	for _, kind := range check {
		if allowed == kind {
			return true
		}
	}
	return false
}

// sendNotificationLocked will iterate through all watchers and notify them that a
// token they are watching has been updated.
//
// NOTE: this function explicitly does not attempt to send the kind or new token value
// along through the channel. With that approach watchers could potentially miss updates
// if the buffered chan fills up. Instead with this approach we just notify that any
// token they care about has been udpated and its up to the caller to retrieve the
// new value (after receiving from the chan). With this approach its entirely possible
// for the watcher to be notified twice before actually retrieving the token after the first
// read from the chan. This is better behavior than missing events. It can cause some
// churn temporarily but in common cases its not expected that these tokens would be updated
// frequently enough to cause this to happen.
func (t *Store) sendNotificationLocked(kinds ...TokenKind) {
	for _, watcher := range t.watchers {
		if !anyKindAllowed(watcher.kind, kinds) {
			// ignore this watcher as it doesn't want events for these kinds of token
			continue
		}

		select {
		case watcher.ch <- struct{}{}:
		default:
			// its already pending a notification
		}
	}
}

// UpdateUserToken replaces the current user token in the store.
// Returns true if it was changed.
func (t *Store) UpdateUserToken(token string, source TokenSource) bool {
	return t.updateToken(token, source, &t.userToken, &t.userTokenSource, TokenKindUser)
}

// UpdateAgentToken replaces the current agent token in the store.
// Returns true if it was changed.
func (t *Store) UpdateAgentToken(token string, source TokenSource) bool {
	return t.updateToken(token, source, &t.agentToken, &t.agentTokenSource, TokenKindAgent)
}

// UpdateAgentRecoveryToken replaces the current agent recovery token in the store.
// Returns true if it was changed.
func (t *Store) UpdateAgentRecoveryToken(token string, source TokenSource) bool {
	return t.updateToken(token, source, &t.agentRecoveryToken,
		&t.agentRecoveryTokenSource, TokenKindAgentRecovery)
}

// UpdateReplicationToken replaces the current replication token in the store.
// Returns true if it was changed.
func (t *Store) UpdateReplicationToken(token string, source TokenSource) bool {
	return t.updateToken(token, source, &t.replicationToken,
		&t.replicationTokenSource, TokenKindReplication)
}

// UpdateConfigFileRegistrationToken replaces the current config file registration token
// in the store. Returns true if it was changed.
func (t *Store) UpdateConfigFileRegistrationToken(token string, source TokenSource) bool {
	return t.updateToken(token, source, &t.configFileRegistrationToken,
		&t.configFileRegistrationTokenSource, TokenKindConfigFileRegistration)
}

// UpdateDNSToken replaces the current DNS token in the store.
// Returns true if it was changed.
func (t *Store) UpdateDNSToken(token string, source TokenSource) bool {
	return t.updateToken(token, source, &t.dnsToken, &t.dnsTokenSource, TokenKindDNS)
}

func (t *Store) updateToken(token string, source TokenSource, dstToken *string, dstSource *TokenSource, kind TokenKind) bool {
	t.l.Lock()
	changed := *dstToken != token || *dstSource != source
	*dstToken = token
	*dstSource = source
	if changed {
		t.sendNotificationLocked(kind)
	}
	t.l.Unlock()
	return changed
}

// UserToken returns the best token to use for user operations.
func (t *Store) UserToken() string {
	t.l.RLock()
	defer t.l.RUnlock()

	return t.userToken
}

// AgentToken returns the best token to use for internal agent operations.
func (t *Store) AgentToken() string {
	t.l.RLock()
	defer t.l.RUnlock()

	if tok := t.enterpriseAgentToken(); tok != "" {
		return tok
	}

	if t.agentToken != "" {
		return t.agentToken
	}
	return t.userToken
}

func (t *Store) AgentRecoveryToken() string {
	t.l.RLock()
	defer t.l.RUnlock()

	return t.agentRecoveryToken
}

// ReplicationToken returns the replication token.
func (t *Store) ReplicationToken() string {
	t.l.RLock()
	defer t.l.RUnlock()

	return t.replicationToken
}

func (t *Store) ConfigFileRegistrationToken() string {
	t.l.RLock()
	defer t.l.RUnlock()

	return t.configFileRegistrationToken
}

func (t *Store) DNSToken() string {
	t.l.RLock()
	defer t.l.RUnlock()

	return t.dnsToken
}

// UserToken returns the best token to use for user operations.
func (t *Store) UserTokenAndSource() (string, TokenSource) {
	t.l.RLock()
	defer t.l.RUnlock()

	return t.userToken, t.userTokenSource
}

// AgentToken returns the best token to use for internal agent operations.
func (t *Store) AgentTokenAndSource() (string, TokenSource) {
	t.l.RLock()
	defer t.l.RUnlock()

	return t.agentToken, t.agentTokenSource
}

func (t *Store) AgentRecoveryTokenAndSource() (string, TokenSource) {
	t.l.RLock()
	defer t.l.RUnlock()

	return t.agentRecoveryToken, t.agentRecoveryTokenSource
}

// ReplicationTokenAndSource returns the replication token and its source.
func (t *Store) ReplicationTokenAndSource() (string, TokenSource) {
	t.l.RLock()
	defer t.l.RUnlock()

	return t.replicationToken, t.replicationTokenSource
}

func (t *Store) ConfigFileRegistrationTokenAndSource() (string, TokenSource) {
	t.l.RLock()
	defer t.l.RUnlock()

	return t.configFileRegistrationToken, t.configFileRegistrationTokenSource
}

// DNSTokenAndSource returns the best token to use for DNS-specific RPC requests and DNS requests
func (t *Store) DNSTokenAndSource() (string, TokenSource) {
	t.l.RLock()
	defer t.l.RUnlock()

	return t.dnsToken, t.dnsTokenSource
}

// IsAgentRecoveryToken checks to see if a given token is the agent recovery token.
// This will never match an empty token for safety.
func (t *Store) IsAgentRecoveryToken(token string) bool {
	t.l.RLock()
	defer t.l.RUnlock()

	return (token != "") && (subtle.ConstantTimeCompare([]byte(token), []byte(t.agentRecoveryToken)) == 1)
}
