package consul

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"strings"
	"sync"
	"time"

	"github.com/hashicorp/go-hclog"
	uuid "github.com/hashicorp/go-uuid"

	"github.com/hashicorp/consul/agent/connect"
	"github.com/hashicorp/consul/agent/connect/ca"
	"github.com/hashicorp/consul/agent/consul/state"
	"github.com/hashicorp/consul/agent/structs"
)

type caState string

const (
	caStateUninitialized     caState = "UNINITIALIZED"
	caStateInitializing              = "INITIALIZING"
	caStateInitialized               = "INITIALIZED"
	caStateRenewIntermediate         = "RENEWING"
	caStateReconfig                  = "RECONFIGURING"
)

// caServerDelegate is an interface for server operations for facilitating
// easier testing.
type caServerDelegate interface {
	State() *state.Store
	IsLeader() bool

	createCAProvider(conf *structs.CAConfiguration) (ca.Provider, error)
	forwardDC(method, dc string, args interface{}, reply interface{}) error
	generateCASignRequest(csr string) *structs.CASignRequest
	raftApply(t structs.MessageType, msg interface{}) (interface{}, error)

	checkServersProvider
}

// CAManager is a wrapper around CA operations such as updating roots, an intermediate
// or the configuration. All operations should go through the CAManager in order to
// avoid data races.
type CAManager struct {
	delegate   caServerDelegate
	serverConf *Config
	logger     hclog.Logger

	providerLock sync.RWMutex
	// provider is the current CA provider in use for Connect. This is
	// only non-nil when we are the leader.
	provider ca.Provider
	// providerRoot is the CARoot that was stored along with the ca.Provider
	// active. It's only updated in lock-step with the provider. This prevents
	// races between state updates to active roots and the fetch of the provider
	// instance.
	providerRoot *structs.CARoot

	// stateLock protects the internal state used for administrative CA tasks.
	stateLock         sync.Mutex
	state             caState
	primaryRoots      structs.IndexedCARoots // The most recently seen state of the root CAs from the primary datacenter.
	actingSecondaryCA bool                   // True if this datacenter has been initialized as a secondary CA.

	leaderRoutineManager *LeaderRoutineManager
}

type caDelegateWithState struct {
	*Server
}

func (c *caDelegateWithState) State() *state.Store {
	return c.fsm.State()
}

func NewCAManager(delegate caServerDelegate, leaderRoutineManager *LeaderRoutineManager, logger hclog.Logger, config *Config) *CAManager {
	return &CAManager{
		delegate:             delegate,
		logger:               logger,
		serverConf:           config,
		state:                caStateUninitialized,
		leaderRoutineManager: leaderRoutineManager,
	}
}

func (c *CAManager) reset() {
	c.state = caStateUninitialized
	c.primaryRoots = structs.IndexedCARoots{}
	c.actingSecondaryCA = false
	c.setCAProvider(nil, nil)
}

// setState attempts to update the CA state to the given state.
// Valid state transitions are:
//
// caStateInitialized -> <any state except caStateInitializing>
// caStateUninitialized -> caStateInitializing
// caStateUninitialized -> caStateReconfig
//
// Other state transitions may be forced if the validateState parameter is set to false.
// This will mainly be used in deferred functions which aim to set the final status based
// a successful/error return.
func (c *CAManager) setState(newState caState, validateState bool) (caState, error) {
	c.stateLock.Lock()
	defer c.stateLock.Unlock()
	state := c.state

	if !validateState ||
		(state == caStateInitialized && newState != caStateInitializing) ||
		(state == caStateUninitialized && newState == caStateInitializing) ||
		(state == caStateUninitialized && newState == caStateReconfig) {
		c.state = newState
	} else {
		return state, &caStateError{Current: state}
	}
	return state, nil
}

type caStateError struct {
	Current caState
}

func (e *caStateError) Error() string {
	return fmt.Sprintf("CA is already in state %q", e.Current)
}

// setPrimaryRoots updates the most recently seen roots from the primary.
func (c *CAManager) setPrimaryRoots(newRoots structs.IndexedCARoots) error {
	c.stateLock.Lock()
	defer c.stateLock.Unlock()

	if c.state == caStateInitializing || c.state == caStateReconfig {
		c.primaryRoots = newRoots
	} else {
		return fmt.Errorf("Cannot update primary roots in state %q", c.state)
	}
	return nil
}

func (c *CAManager) secondaryGetActivePrimaryCARoot() (*structs.CARoot, error) {
	// TODO: this could be a different lock, as long as its the same lock in secondarySetPrimaryRoots
	c.stateLock.Lock()
	primaryRoots := c.primaryRoots
	c.stateLock.Unlock()

	for _, root := range primaryRoots.Roots {
		if root.ID == primaryRoots.ActiveRootID && root.Active {
			return root, nil
		}
	}
	return nil, fmt.Errorf("primary datacenter does not have an active root CA for Connect")
}

// initializeCAConfig is used to initialize the CA config if necessary
// when setting up the CA during establishLeadership. The state should be set to
// non-ready before calling this.
func (c *CAManager) initializeCAConfig() (*structs.CAConfiguration, error) {
	state := c.delegate.State()
	_, config, err := state.CAConfig(nil)
	if err != nil {
		return nil, err
	}
	if config == nil {
		config = c.serverConf.CAConfig
		if config.ClusterID == "" {
			id, err := uuid.GenerateUUID()
			if err != nil {
				return nil, err
			}
			config.ClusterID = id
		}
	} else if _, ok := config.Config["IntermediateCertTTL"]; !ok {
		dup := *config
		copied := make(map[string]interface{})
		for k, v := range dup.Config {
			copied[k] = v
		}
		copied["IntermediateCertTTL"] = connect.DefaultIntermediateCertTTL.String()
		dup.Config = copied
		config = &dup
	} else {
		return config, nil
	}

	req := structs.CARequest{
		Op:     structs.CAOpSetConfig,
		Config: config,
	}
	if resp, err := c.delegate.raftApply(structs.ConnectCARequestType, req); err != nil {
		return nil, err
	} else if respErr, ok := resp.(error); ok {
		return nil, respErr
	}

	return config, nil
}

// newCARoot returns a filled-in structs.CARoot from a raw PEM value.
func newCARoot(pemValue, provider, clusterID string) (*structs.CARoot, error) {
	primaryCert, err := connect.ParseCert(pemValue)
	if err != nil {
		return nil, err
	}
	keyType, keyBits, err := connect.KeyInfoFromCert(primaryCert)
	if err != nil {
		return nil, fmt.Errorf("error extracting root key info: %v", err)
	}
	return &structs.CARoot{
		ID:                  connect.CalculateCertFingerprint(primaryCert.Raw),
		Name:                fmt.Sprintf("%s CA Primary Cert", strings.Title(provider)),
		SerialNumber:        primaryCert.SerialNumber.Uint64(),
		SigningKeyID:        connect.EncodeSigningKeyID(primaryCert.SubjectKeyId),
		ExternalTrustDomain: clusterID,
		NotBefore:           primaryCert.NotBefore,
		NotAfter:            primaryCert.NotAfter,
		RootCert:            pemValue,
		PrivateKeyType:      keyType,
		PrivateKeyBits:      keyBits,
		Active:              true,
	}, nil
}

// getCAProvider returns the currently active instance of the CA Provider,
// as well as the active root.
func (c *CAManager) getCAProvider() (ca.Provider, *structs.CARoot) {
	retries := 0
	var result ca.Provider
	var resultRoot *structs.CARoot
	for result == nil {
		c.providerLock.RLock()
		result = c.provider
		resultRoot = c.providerRoot
		c.providerLock.RUnlock()

		// In cases where an agent is started with managed proxies, we may ask
		// for the provider before establishLeadership completes. If we're the
		// leader, then wait and get the provider again
		if result == nil && c.delegate.IsLeader() && retries < 10 {
			retries++
			time.Sleep(50 * time.Millisecond)
			continue
		}

		break
	}

	return result, resultRoot
}

// setCAProvider is being called while holding the stateLock
// which means it must never take that lock itself or call anything that does.
func (c *CAManager) setCAProvider(newProvider ca.Provider, root *structs.CARoot) {
	c.providerLock.Lock()
	c.provider = newProvider
	c.providerRoot = root
	c.providerLock.Unlock()
}

func (c *CAManager) Start() {
	// Attempt to initialize the Connect CA now. This will
	// happen during leader establishment and it would be great
	// if the CA was ready to go once that process was finished.
	if err := c.Initialize(); err != nil {
		c.logger.Error("Failed to initialize Connect CA", "error", err)

		// we failed to fully initialize the CA so we need to spawn a
		// go routine to retry this process until it succeeds or we lose
		// leadership and the go routine gets stopped.
		c.leaderRoutineManager.Start(backgroundCAInitializationRoutineName, c.backgroundCAInitialization)
	} else {
		// We only start these if CA initialization was successful. If not the completion of the
		// background CA initialization will start these routines.
		c.startPostInitializeRoutines()
	}
}

func (c *CAManager) Stop() {
	c.leaderRoutineManager.Stop(secondaryCARootWatchRoutineName)
	c.leaderRoutineManager.Stop(intermediateCertRenewWatchRoutineName)
	c.leaderRoutineManager.Stop(backgroundCAInitializationRoutineName)
}

func (c *CAManager) startPostInitializeRoutines() {
	// Start the Connect secondary DC actions if enabled.
	if c.serverConf.Datacenter != c.serverConf.PrimaryDatacenter {
		c.leaderRoutineManager.Start(secondaryCARootWatchRoutineName, c.secondaryCARootWatch)
	}

	c.leaderRoutineManager.Start(intermediateCertRenewWatchRoutineName, c.runRenewIntermediate)
}

func (c *CAManager) backgroundCAInitialization(ctx context.Context) error {
	retryLoopBackoffAbortOnSuccess(ctx, c.Initialize, func(err error) {
		c.logger.Error("Failed to initialize Connect CA",
			"routine", backgroundCAInitializationRoutineName,
			"error", err,
		)
	})

	if err := ctx.Err(); err != nil {
		return err
	}

	c.logger.Info("Successfully initialized the Connect CA")

	c.startPostInitializeRoutines()
	return nil
}

// Initialize sets up the CA provider when gaining leadership, either bootstrapping
// the CA if this is the primary DC or making a remote RPC for intermediate signing
// if this is a secondary DC.
func (c *CAManager) Initialize() (reterr error) {
	// Bail if connect isn't enabled.
	if !c.serverConf.ConnectEnabled {
		return nil
	}

	// Update the state before doing anything else.
	_, err := c.setState(caStateInitializing, true)
	var errCaState *caStateError
	switch {
	case errors.As(err, &errCaState) && errCaState.Current == caStateInitialized:
		return nil
	case err != nil:
		return err
	}

	defer func() {
		// Using named return values in deferred funcs isnt too common in our code
		// but it is first class Go functionality. The error erturned from the
		// main func will be available by its given name within deferred functions.
		// See: https://blog.golang.org/defer-panic-and-recover
		if reterr == nil {
			c.setState(caStateInitialized, false)
		} else {
			c.setState(caStateUninitialized, false)
		}
	}()

	// Initialize the provider based on the current config.
	conf, err := c.initializeCAConfig()
	if err != nil {
		return err
	}
	provider, err := c.delegate.createCAProvider(conf)
	if err != nil {
		return err
	}

	c.setCAProvider(provider, nil)

	// Run the root CA initialization if this is the primary DC.
	if c.serverConf.PrimaryDatacenter == c.serverConf.Datacenter {
		return c.initializeRootCA(provider, conf)
	}

	// If this isn't the primary DC, run the secondary DC routine if the primary has already been upgraded to at least 1.6.0
	versionOk, foundPrimary := ServersInDCMeetMinimumVersion(c.delegate, c.serverConf.PrimaryDatacenter, minMultiDCConnectVersion)
	if !foundPrimary {
		c.logger.Warn("primary datacenter is configured but unreachable - deferring initialization of the secondary datacenter CA")
		// return nil because we will initialize the secondary CA later
		return nil
	} else if !versionOk {
		// return nil because we will initialize the secondary CA later
		c.logger.Warn("servers in the primary datacenter are not at least at the minimum version - deferring initialization of the secondary datacenter CA",
			"min_version", minMultiDCConnectVersion.String(),
		)
		return nil
	}

	// Get the root CA to see if we need to refresh our intermediate.
	args := structs.DCSpecificRequest{
		Datacenter: c.serverConf.PrimaryDatacenter,
	}
	var roots structs.IndexedCARoots
	if err := c.delegate.forwardDC("ConnectCA.Roots", c.serverConf.PrimaryDatacenter, &args, &roots); err != nil {
		return fmt.Errorf("failed to get CA roots from primary DC: %w", err)
	}
	if err := c.setPrimaryRoots(roots); err != nil {
		return err
	}

	// Configure the CA provider and initialize the intermediate certificate if necessary.
	if err := c.initializeSecondaryProvider(provider, roots); err != nil {
		return fmt.Errorf("error configuring provider: %v", err)
	}
	if err := c.initializeSecondaryCA(provider, nil); err != nil {
		return err
	}

	c.logger.Info("initialized secondary datacenter CA with provider", "provider", conf.Provider)
	return nil
}

// initializeRootCA runs the initialization logic for a root CA. It should only
// be called while the state lock is held by setting the state to non-ready.
func (c *CAManager) initializeRootCA(provider ca.Provider, conf *structs.CAConfiguration) error {
	pCfg := ca.ProviderConfig{
		ClusterID:  conf.ClusterID,
		Datacenter: c.serverConf.Datacenter,
		IsPrimary:  true,
		RawConfig:  conf.Config,
		State:      conf.State,
	}
	if err := provider.Configure(pCfg); err != nil {
		return fmt.Errorf("error configuring provider: %v", err)
	}
	root, err := provider.GenerateRoot()
	if err != nil {
		return fmt.Errorf("error generating CA root certificate: %v", err)
	}

	rootCA, err := newCARoot(root.PEM, conf.Provider, conf.ClusterID)
	if err != nil {
		return err
	}

	// TODO: https://github.com/hashicorp/consul/issues/12386
	interPEM, err := provider.GenerateIntermediate()
	if err != nil {
		return fmt.Errorf("error generating intermediate cert: %v", err)
	}
	intermediateCert, err := connect.ParseCert(interPEM)
	if err != nil {
		return fmt.Errorf("error getting intermediate cert: %v", err)
	}

	// If the provider has state to persist and it's changed or new then update
	// CAConfig.
	pState, err := provider.State()
	if err != nil {
		return fmt.Errorf("error getting provider state: %v", err)
	}
	if !reflect.DeepEqual(conf.State, pState) {
		// Update the CAConfig in raft to persist the provider state
		conf.State = pState
		req := structs.CARequest{
			Op:     structs.CAOpSetConfig,
			Config: conf,
		}
		if _, err = c.delegate.raftApply(structs.ConnectCARequestType, req); err != nil {
			return fmt.Errorf("error persisting provider state: %v", err)
		}
	}

	var rootUpdateRequired bool

	// Versions prior to 1.9.3, 1.8.8, and 1.7.12 incorrectly used the primary
	// rootCA's subjectKeyID here instead of the intermediate. For
	// provider=consul this didn't matter since there are no intermediates in
	// the primaryDC, but for vault it does matter.
	expectedSigningKeyID := connect.EncodeSigningKeyID(intermediateCert.SubjectKeyId)
	if rootCA.SigningKeyID != expectedSigningKeyID {
		c.logger.Info("Correcting stored CARoot values",
			"previous-signing-key", rootCA.SigningKeyID, "updated-signing-key", expectedSigningKeyID)
		rootCA.SigningKeyID = expectedSigningKeyID
		rootUpdateRequired = true
	}

	// Add the local leaf signing cert to the rootCA struct. This handles both
	// upgrades of existing state, and new rootCA.
	if c.getLeafSigningCertFromRoot(rootCA) != interPEM {
		rootCA.IntermediateCerts = append(rootCA.IntermediateCerts, interPEM)
		rootUpdateRequired = true
	}

	// Check if the CA root is already initialized and exit if it is,
	// adding on any existing intermediate certs since they aren't directly
	// tied to the provider.
	// Every change to the CA after this initial bootstrapping should
	// be done through the rotation process.
	state := c.delegate.State()
	_, activeRoot, err := state.CARootActive(nil)
	if err != nil {
		return err
	}
	if activeRoot != nil && !rootUpdateRequired {
		// This state shouldn't be possible to get into because we update the root and
		// CA config in the same FSM operation.
		if activeRoot.ID != rootCA.ID {
			return fmt.Errorf("stored CA root %q is not the active root (%s)", rootCA.ID, activeRoot.ID)
		}

		// TODO: why doesn't this c.setCAProvider(provider, activeRoot) ?
		rootCA.IntermediateCerts = activeRoot.IntermediateCerts
		c.setCAProvider(provider, rootCA)

		c.logger.Info("initialized primary datacenter CA from existing CARoot with provider", "provider", conf.Provider)
		return nil
	}

	// Get the highest index
	idx, _, err := state.CARoots(nil)
	if err != nil {
		return err
	}

	// Store the root cert in raft
	resp, err := c.delegate.raftApply(structs.ConnectCARequestType, &structs.CARequest{
		Op:    structs.CAOpSetRoots,
		Index: idx,
		Roots: []*structs.CARoot{rootCA},
	})
	if err != nil {
		c.logger.Error("Raft apply failed", "error", err)
		return err
	}
	if respErr, ok := resp.(error); ok {
		return respErr
	}

	c.setCAProvider(provider, rootCA)

	c.logger.Info("initialized primary datacenter CA with provider", "provider", conf.Provider)

	return nil
}

// getLeafSigningCertFromRoot returns the PEM encoded certificate that should be used to
// sign leaf certificates in the local datacenter. The SubjectKeyId of the
// returned cert should always match the SigningKeyID of the CARoot.
//
// TODO: fix the data model so that we don't need this complicated lookup to
// find the leaf signing cert. See github.com/hashicorp/consul/issues/11347.
func (c *CAManager) getLeafSigningCertFromRoot(root *structs.CARoot) string {
	if !c.isIntermediateUsedToSignLeaf() {
		return root.RootCert
	}
	if len(root.IntermediateCerts) == 0 {
		return ""
	}
	return root.IntermediateCerts[len(root.IntermediateCerts)-1]
}

// initializeSecondaryCA runs the routine for generating an intermediate CA CSR and getting
// it signed by the primary DC if the root CA of the primary DC has changed since the last
// intermediate. It should only be called while the state lock is held by setting the state
// to non-ready.
func (c *CAManager) initializeSecondaryCA(provider ca.Provider, config *structs.CAConfiguration) error {
	activeIntermediate, err := provider.ActiveIntermediate()
	if err != nil {
		return err
	}

	_, activeRoot, err := c.delegate.State().CARootActive(nil)
	if err != nil {
		return err
	}
	var currentSigningKeyID string
	if activeRoot != nil {
		currentSigningKeyID = activeRoot.SigningKeyID
	}

	var expectedSigningKeyID string
	if activeIntermediate != "" {
		intermediateCert, err := connect.ParseCert(activeIntermediate)
		if err != nil {
			return fmt.Errorf("error parsing active intermediate cert: %v", err)
		}
		expectedSigningKeyID = connect.EncodeSigningKeyID(intermediateCert.SubjectKeyId)
	}

	newActiveRoot, err := c.secondaryGetActivePrimaryCARoot()
	if err != nil {
		return err
	}

	// Get a signed intermediate from the primary DC if the provider
	// hasn't been initialized yet or if the primary's root has changed.
	needsNewIntermediate := activeIntermediate == ""
	if activeRoot != nil && newActiveRoot.ID != activeRoot.ID {
		needsNewIntermediate = true
	}

	// Also we take this opportunity to correct an incorrectly persisted SigningKeyID
	// in secondary datacenters (see PR-6513).
	if expectedSigningKeyID != "" && currentSigningKeyID != expectedSigningKeyID {
		needsNewIntermediate = true
	}

	if needsNewIntermediate {
		if err := c.secondaryRequestNewSigningCert(provider, newActiveRoot); err != nil {
			return err
		}
	} else {
		// Discard the primary's representation since our local one is
		// sufficiently up to date.
		newActiveRoot = activeRoot
	}

	// Determine whether a root update is needed, and persist the roots/config accordingly.
	var newRoot *structs.CARoot
	if activeRoot == nil || needsNewIntermediate {
		newRoot = newActiveRoot
	}
	if err := c.persistNewRootAndConfig(provider, newRoot, config); err != nil {
		return err
	}

	c.setCAProvider(provider, newActiveRoot)
	return nil
}

// persistNewRootAndConfig should only be called while the state lock is held
// by setting the state to non-ready.
// If newActiveRoot is non-nil, it will be appended to the current roots list.
// If config is non-nil, it will be used to overwrite the existing config.
func (c *CAManager) persistNewRootAndConfig(provider ca.Provider, newActiveRoot *structs.CARoot, config *structs.CAConfiguration) error {
	state := c.delegate.State()
	idx, oldRoots, err := state.CARoots(nil)
	if err != nil {
		return err
	}

	// Look up the existing CA config if a new one wasn't provided.
	var newConf structs.CAConfiguration
	_, storedConfig, err := state.CAConfig(nil)
	if err != nil {
		return err
	}
	if storedConfig == nil {
		return fmt.Errorf("local CA not initialized yet")
	}
	// Exit early if the change is a no-op.
	if newActiveRoot == nil && config != nil && config.Provider == storedConfig.Provider && reflect.DeepEqual(config.Config, storedConfig.Config) {
		return nil
	}

	if config != nil {
		newConf = *config
	} else {
		newConf = *storedConfig
	}

	// Update the trust domain for the config if there's a new root, or keep the old
	// one if the root isn't being updated.
	newConf.ModifyIndex = storedConfig.ModifyIndex
	if newActiveRoot != nil {
		newConf.ClusterID = newActiveRoot.ExternalTrustDomain
	} else {
		_, activeRoot, err := state.CARootActive(nil)
		if err != nil {
			return err
		}
		newConf.ClusterID = activeRoot.ExternalTrustDomain
	}

	// Persist any state the provider needs us to
	newConf.State, err = provider.State()
	if err != nil {
		return fmt.Errorf("error getting provider state: %v", err)
	}

	// If there's a new active root, copy the root list and append it, updating
	// the old root with the time it was rotated out.
	var newRoots structs.CARoots
	for _, r := range oldRoots {
		newRoot := *r
		if newRoot.Active && newActiveRoot != nil {
			newRoot.Active = false
			newRoot.RotatedOutAt = time.Now()
		}
		if newRoot.ExternalTrustDomain == "" {
			newRoot.ExternalTrustDomain = newConf.ClusterID
		}
		newRoots = append(newRoots, &newRoot)
	}
	if newActiveRoot != nil {
		newRoots = append(newRoots, newActiveRoot)
	}

	args := &structs.CARequest{
		Op:     structs.CAOpSetRootsAndConfig,
		Index:  idx,
		Roots:  newRoots,
		Config: &newConf,
	}
	resp, err := c.delegate.raftApply(structs.ConnectCARequestType, args)
	if err != nil {
		return err
	}
	if respErr, ok := resp.(error); ok {
		return respErr
	}
	if respOk, ok := resp.(bool); ok && !respOk {
		return fmt.Errorf("could not atomically update roots and config")
	}

	c.logger.Info("updated root certificates from primary datacenter")
	return nil
}

func (c *CAManager) UpdateConfiguration(args *structs.CARequest) (reterr error) {
	// Attempt to update the state first.
	oldState, err := c.setState(caStateReconfig, true)
	if err != nil {
		return err
	}
	defer func() {
		// Using named return values in deferred funcs isnt too common in our code
		// but it is first class Go functionality. The error erturned from the
		// main func will be available by its given name within deferred functions.
		// See: https://blog.golang.org/defer-panic-and-recover
		if reterr == nil {
			c.setState(caStateInitialized, false)
		} else {
			c.setState(oldState, false)
		}
	}()

	// Attempt to initialize the config if we failed to do so in Initialize for some reason
	_, err = c.initializeCAConfig()
	if err != nil {
		return err
	}

	// Exit early if it's a no-op change
	state := c.delegate.State()
	confIdx, config, err := state.CAConfig(nil)
	if err != nil {
		return err
	}

	// Don't allow state changes. Either it needs to be empty or the same to allow
	// read-modify-write loops that don't touch the State field.
	if len(args.Config.State) > 0 &&
		!reflect.DeepEqual(args.Config.State, config.State) {
		return ErrStateReadOnly
	}

	// Don't allow users to change the ClusterID.
	args.Config.ClusterID = config.ClusterID
	if args.Config.Provider == config.Provider && reflect.DeepEqual(args.Config.Config, config.Config) {
		return nil
	}

	// If the provider hasn't changed, we need to load the current Provider state
	// so it can decide if it needs to change resources or not based on the config
	// change.
	if args.Config.Provider == config.Provider {
		// Note this is a shallow copy since the State method doc requires the
		// provider return a map that will not be further modified and should not
		// modify the one we pass to Configure.
		args.Config.State = config.State
	}

	// Create a new instance of the provider described by the config
	// and get the current active root CA. This acts as a good validation
	// of the config and makes sure the provider is functioning correctly
	// before we commit any changes to Raft.
	newProvider, err := c.delegate.createCAProvider(args.Config)
	if err != nil {
		return fmt.Errorf("could not initialize provider: %v", err)
	}
	pCfg := ca.ProviderConfig{
		ClusterID:  args.Config.ClusterID,
		Datacenter: c.serverConf.Datacenter,
		// This endpoint can be called in a secondary DC too so set this correctly.
		IsPrimary: c.serverConf.Datacenter == c.serverConf.PrimaryDatacenter,
		RawConfig: args.Config.Config,
		State:     args.Config.State,
	}
	if err := newProvider.Configure(pCfg); err != nil {
		return fmt.Errorf("error configuring provider: %v", err)
	}

	// Set up a defer to clean up the new provider if we exit early due to an error.
	cleanupNewProvider := true
	defer func() {
		if cleanupNewProvider {
			if err := newProvider.Cleanup(args.Config.Provider != config.Provider, args.Config.Config); err != nil {
				c.logger.Warn("failed to clean up CA provider while handling startup failure", "provider", newProvider, "error", err)
			}
		}
	}()

	// If this is a secondary, just check if the intermediate needs to be regenerated.
	if c.serverConf.Datacenter != c.serverConf.PrimaryDatacenter {
		if err := c.initializeSecondaryCA(newProvider, args.Config); err != nil {
			return fmt.Errorf("Error updating secondary datacenter CA config: %v", err)
		}
		cleanupNewProvider = false
		c.logger.Info("Secondary CA provider config updated")
		return nil
	}

	providerRoot, err := newProvider.GenerateRoot()
	if err != nil {
		return fmt.Errorf("error generating CA root certificate: %v", err)
	}

	newRootPEM := providerRoot.PEM
	newActiveRoot, err := newCARoot(newRootPEM, args.Config.Provider, args.Config.ClusterID)
	if err != nil {
		return err
	}

	// See if the provider needs to persist any state along with the config
	pState, err := newProvider.State()
	if err != nil {
		return fmt.Errorf("error getting provider state: %v", err)
	}
	args.Config.State = pState

	// Compare the new provider's root CA ID to the current one. If they
	// match, just update the existing provider with the new config.
	// If they don't match, begin the root rotation process.
	_, root, err := state.CARootActive(nil)
	if err != nil {
		return err
	}

	// If the root didn't change, just update the config and return.
	if root != nil && root.ID == newActiveRoot.ID {
		args.Op = structs.CAOpSetConfig
		resp, err := c.delegate.raftApply(structs.ConnectCARequestType, args)
		if err != nil {
			return err
		}
		if respErr, ok := resp.(error); ok {
			return respErr
		}

		// If the config has been committed, update the local provider instance
		cleanupNewProvider = false
		c.setCAProvider(newProvider, newActiveRoot)

		c.logger.Info("CA provider config updated")

		return nil
	}

	// get the old CA provider to be used for Cross Signing and to clean it up at the end
	// of the functi8on.
	oldProvider, _ := c.getCAProvider()
	if oldProvider == nil {
		return fmt.Errorf("internal error: CA provider is nil")
	}

	// We only even think about cross signing if the current provider has a root cert
	// In some cases such as having a bad CA configuration during startup the provider
	// may not have been able to generate a cert. We then want to be able to prevent
	// an attempt to cross sign the cert which will definitely fail.
	if root != nil {
		// If it's a config change that would trigger a rotation (different provider/root):
		// 1. Get the root from the new provider.
		// 2. Call CrossSignCA on the old provider to sign the new root with the old one to
		// get a cross-signed certificate.
		// 3. Take the active root for the new provider and append the intermediate from step 2
		// to its list of intermediates.
		// TODO: this cert is already parsed once in newCARoot, could we remove the second parse?
		newRoot, err := connect.ParseCert(newRootPEM)
		if err != nil {
			return err
		}

		// At this point, we know the config change has triggered a root rotation,
		// either by swapping the provider type or changing the provider's config
		// to use a different root certificate.

		// First up, check that the current provider actually supports
		// cross-signing.
		canXSign, err := oldProvider.SupportsCrossSigning()
		if err != nil {
			return fmt.Errorf("CA provider error: %s", err)
		}
		if !canXSign && !args.Config.ForceWithoutCrossSigning {
			return errors.New("The current CA Provider does not support cross-signing. " +
				"You can try again with ForceWithoutCrossSigningSet but this may cause " +
				"disruption - see documentation for more.")
		}
		if args.Config.ForceWithoutCrossSigning {
			c.logger.Warn("ForceWithoutCrossSigning set, CA reconfiguration skipping cross-signing")
		}

		// If ForceWithoutCrossSigning wasn't set, attempt to have the old CA generate a
		// cross-signed intermediate.
		if canXSign && !args.Config.ForceWithoutCrossSigning {
			// Have the old provider cross-sign the new root
			xcCert, err := oldProvider.CrossSignCA(newRoot)
			if err != nil {
				return err
			}

			// Add the cross signed cert to the new CA's intermediates (to be attached
			// to leaf certs).
			newActiveRoot.IntermediateCerts = []string{xcCert}
		}
	}

	// TODO: https://github.com/hashicorp/consul/issues/12386
	intermediate, err := newProvider.GenerateIntermediate()
	if err != nil {
		return err
	}
	if intermediate != newRootPEM {
		newActiveRoot.IntermediateCerts = append(newActiveRoot.IntermediateCerts, intermediate)
	}

	// Update the roots and CA config in the state store at the same time
	idx, roots, err := state.CARoots(nil)
	if err != nil {
		return err
	}

	var newRoots structs.CARoots
	for _, r := range roots {
		newRoot := *r
		if newRoot.Active {
			newRoot.Active = false
			newRoot.RotatedOutAt = time.Now()
		}
		newRoots = append(newRoots, &newRoot)
	}
	newRoots = append(newRoots, newActiveRoot)

	args.Op = structs.CAOpSetRootsAndConfig
	args.Index = idx
	args.Config.ModifyIndex = confIdx
	args.Roots = newRoots
	resp, err := c.delegate.raftApply(structs.ConnectCARequestType, args)
	if err != nil {
		return err
	}
	if respErr, ok := resp.(error); ok {
		return respErr
	}
	if respOk, ok := resp.(bool); ok && !respOk {
		return fmt.Errorf("could not atomically update roots and config")
	}

	// If the config has been committed, update the local provider instance
	// and call teardown on the old provider
	cleanupNewProvider = false
	c.setCAProvider(newProvider, newActiveRoot)

	if err := oldProvider.Cleanup(args.Config.Provider != config.Provider, args.Config.Config); err != nil {
		c.logger.Warn("failed to clean up old provider", "provider", config.Provider, "error", err)
	}

	c.logger.Info("CA rotated to new root under provider", "provider", args.Config.Provider)

	return nil
}

// getIntermediateCAPrimary regenerates the intermediate cert in the primary datacenter.
// This is only run for CAs that require an intermediary in the primary DC, such as Vault.
//  It should only be called while the state lock is held by setting the state to non-ready.
func (c *CAManager) getIntermediateCAPrimary(provider ca.Provider, newActiveRoot *structs.CARoot) error {
	// Generate and sign an intermediate cert using the root CA.
	intermediatePEM, err := provider.GenerateIntermediate()
	if err != nil {
		return fmt.Errorf("error generating new intermediate cert: %v", err)
	}

	intermediateCert, err := connect.ParseCert(intermediatePEM)
	if err != nil {
		return fmt.Errorf("error parsing intermediate cert: %v", err)
	}

	// Append the new intermediate to our local active root entry. This is
	// where the root representations start to diverge.
	newActiveRoot.IntermediateCerts = append(newActiveRoot.IntermediateCerts, intermediatePEM)
	newActiveRoot.SigningKeyID = connect.EncodeSigningKeyID(intermediateCert.SubjectKeyId)

	c.logger.Info("generated new intermediate certificate for primary datacenter")
	return nil
}

// secondaryRequestNewSigningCert creates a Certificate Signing Request, sends
// the request to the primary, and stores the received certificate in the
// provider.
// Should only be called while the state lock is held by setting the state to non-ready.
func (c *CAManager) secondaryRequestNewSigningCert(provider ca.Provider, newActiveRoot *structs.CARoot) error {
	csr, err := provider.GenerateIntermediateCSR()
	if err != nil {
		return err
	}

	var intermediatePEM string
	if err := c.delegate.forwardDC("ConnectCA.SignIntermediate", c.serverConf.PrimaryDatacenter, c.delegate.generateCASignRequest(csr), &intermediatePEM); err != nil {
		// this is a failure in the primary and shouldn't be capable of erroring out our establishing leadership
		c.logger.Warn("Primary datacenter refused to sign our intermediate CA certificate", "error", err)
		return nil
	}

	if err := provider.SetIntermediate(intermediatePEM, newActiveRoot.RootCert); err != nil {
		return fmt.Errorf("Failed to set the intermediate certificate with the CA provider: %v", err)
	}

	intermediateCert, err := connect.ParseCert(intermediatePEM)
	if err != nil {
		return fmt.Errorf("error parsing intermediate cert: %v", err)
	}

	// Append the new intermediate to our local active root entry. This is
	// where the root representations start to diverge.
	newActiveRoot.IntermediateCerts = append(newActiveRoot.IntermediateCerts, intermediatePEM)
	newActiveRoot.SigningKeyID = connect.EncodeSigningKeyID(intermediateCert.SubjectKeyId)

	c.logger.Info("received new intermediate certificate from primary datacenter")
	return nil
}

// setLeafSigningCert updates the CARoot by appending the pem to the list of
// intermediate certificates, and setting the SigningKeyID to the encoded
// SubjectKeyId of the certificate.
func setLeafSigningCert(caRoot *structs.CARoot, pem string) error {
	cert, err := connect.ParseCert(pem)
	if err != nil {
		return fmt.Errorf("error parsing leaf signing cert: %w", err)
	}

	caRoot.IntermediateCerts = append(caRoot.IntermediateCerts, pem)
	caRoot.SigningKeyID = connect.EncodeSigningKeyID(cert.SubjectKeyId)
	return nil
}

// runRenewIntermediate periodically attempts to renew the intermediate cert.
func (c *CAManager) runRenewIntermediate(ctx context.Context) error {
	isPrimary := c.serverConf.Datacenter == c.serverConf.PrimaryDatacenter

	for {
		select {
		case <-ctx.Done():
			return nil
		case <-time.After(structs.IntermediateCertRenewInterval):
			retryLoopBackoffAbortOnSuccess(ctx, func() error {
				return c.RenewIntermediate(ctx, isPrimary)
			}, func(err error) {
				c.logger.Error("error renewing intermediate certs",
					"routine", intermediateCertRenewWatchRoutineName,
					"error", err,
				)
			})
		}
	}
}

// RenewIntermediate checks the intermediate cert for
// expiration. If more than half the time a cert is valid has passed,
// it will try to renew it.
func (c *CAManager) RenewIntermediate(ctx context.Context, isPrimary bool) error {
	// Grab the 'lock' right away so the provider/config can't be changed out while we check
	// the intermediate.
	if _, err := c.setState(caStateRenewIntermediate, true); err != nil {
		return err
	}
	defer c.setState(caStateInitialized, false)

	provider, _ := c.getCAProvider()
	if provider == nil {
		// this happens when leadership is being revoked and this go routine will be stopped
		return nil
	}
	// If this isn't the primary, make sure the CA has been initialized.
	if !isPrimary && !c.configuredSecondaryCA() {
		return fmt.Errorf("secondary CA is not yet configured.")
	}

	state := c.delegate.State()
	_, root, err := state.CARootActive(nil)
	if err != nil {
		return err
	}
	activeRoot := root.Clone()

	// If this is the primary, check if this is a provider that uses an intermediate cert. If
	// it isn't, we don't need to check for a renewal.
	if isPrimary && !primaryUsesIntermediate(provider) {
		return nil
	}

	activeIntermediate, err := provider.ActiveIntermediate()
	if err != nil {
		return err
	}

	if activeIntermediate == "" {
		return fmt.Errorf("datacenter doesn't have an active intermediate.")
	}

	intermediateCert, err := connect.ParseCert(activeIntermediate)
	if err != nil {
		return fmt.Errorf("error parsing active intermediate cert: %v", err)
	}

	if lessThanHalfTimePassed(time.Now(), intermediateCert.NotBefore, intermediateCert.NotAfter) {
		return nil
	}

	// Enough time has passed, go ahead with getting a new intermediate.
	renewalFunc := c.getIntermediateCAPrimary
	if !isPrimary {
		renewalFunc = c.secondaryRequestNewSigningCert
	}
	errCh := make(chan error, 1)
	go func() {
		errCh <- renewalFunc(provider, activeRoot)
	}()

	// Wait for the renewal func to return or for the context to be canceled.
	select {
	case <-ctx.Done():
		return ctx.Err()
	case err := <-errCh:
		if err != nil {
			return err
		}
	}

	if err := c.persistNewRootAndConfig(provider, activeRoot, nil); err != nil {
		return err
	}

	c.setCAProvider(provider, activeRoot)
	return nil
}

// secondaryCARootWatch maintains a blocking query to the primary datacenter's
// ConnectCA.Roots endpoint to monitor when it needs to request a new signed
// intermediate certificate.
func (c *CAManager) secondaryCARootWatch(ctx context.Context) error {
	args := structs.DCSpecificRequest{
		Datacenter: c.serverConf.PrimaryDatacenter,
		QueryOptions: structs.QueryOptions{
			// the maximum time the primary roots watch query can block before returning
			MaxQueryTime: c.serverConf.MaxQueryTime,
		},
	}

	c.logger.Debug("starting Connect CA root replication from primary datacenter", "primary", c.serverConf.PrimaryDatacenter)

	retryLoopBackoff(ctx, func() error {
		var roots structs.IndexedCARoots
		if err := c.delegate.forwardDC("ConnectCA.Roots", c.serverConf.PrimaryDatacenter, &args, &roots); err != nil {
			return fmt.Errorf("Error retrieving the primary datacenter's roots: %v", err)
		}

		// Return if the context has been canceled while waiting on the RPC.
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		// Attempt to update the roots using the returned data.
		if err := c.UpdateRoots(roots); err != nil {
			return err
		}
		args.QueryOptions.MinQueryIndex = nextIndexVal(args.QueryOptions.MinQueryIndex, roots.QueryMeta.Index)
		return nil
	}, func(err error) {
		c.logger.Error("CA root replication failed, will retry",
			"routine", secondaryCARootWatchRoutineName,
			"error", err,
		)
	})

	return nil
}

// UpdateRoots updates the cached roots from the primary and regenerates the intermediate
// certificate if necessary.
func (c *CAManager) UpdateRoots(roots structs.IndexedCARoots) error {
	// Update the state first to claim the 'lock'.
	if _, err := c.setState(caStateReconfig, true); err != nil {
		return err
	}
	defer c.setState(caStateInitialized, false)

	// Update the cached primary roots now that the lock is held.
	if err := c.setPrimaryRoots(roots); err != nil {
		return err
	}

	// Check to see if the primary has been upgraded in case we're waiting to switch to
	// secondary mode.
	provider, _ := c.getCAProvider()
	if provider == nil {
		// this happens when leadership is being revoked and this go routine will be stopped
		return nil
	}
	if !c.configuredSecondaryCA() {
		versionOk, primaryFound := ServersInDCMeetMinimumVersion(c.delegate, c.serverConf.PrimaryDatacenter, minMultiDCConnectVersion)
		if !primaryFound {
			return fmt.Errorf("Primary datacenter is unreachable - deferring secondary CA initialization")
		}

		if versionOk {
			if err := c.initializeSecondaryProvider(provider, roots); err != nil {
				return fmt.Errorf("Failed to initialize secondary CA provider: %v", err)
			}
		}
	}

	// Run the secondary CA init routine to see if we need to request a new
	// intermediate.
	if c.configuredSecondaryCA() {
		if err := c.initializeSecondaryCA(provider, nil); err != nil {
			return fmt.Errorf("Failed to initialize the secondary CA: %v", err)
		}
	}

	return nil
}

// initializeSecondaryProvider configures the given provider for a secondary, non-root datacenter.
func (c *CAManager) initializeSecondaryProvider(provider ca.Provider, roots structs.IndexedCARoots) error {
	if roots.TrustDomain == "" {
		return fmt.Errorf("trust domain from primary datacenter is not initialized")
	}

	clusterID := strings.Split(roots.TrustDomain, ".")[0]
	_, conf, err := c.delegate.State().CAConfig(nil)
	if err != nil {
		return err
	}

	pCfg := ca.ProviderConfig{
		ClusterID:  clusterID,
		Datacenter: c.serverConf.Datacenter,
		IsPrimary:  false,
		RawConfig:  conf.Config,
		State:      conf.State,
	}
	if err := provider.Configure(pCfg); err != nil {
		return fmt.Errorf("error configuring provider: %v", err)
	}

	return c.setSecondaryCA()
}

// setSecondaryCA sets the flag for acting as a secondary CA to true.
func (c *CAManager) setSecondaryCA() error {
	c.stateLock.Lock()
	defer c.stateLock.Unlock()

	if c.state == caStateInitializing || c.state == caStateReconfig {
		c.actingSecondaryCA = true
	} else {
		return fmt.Errorf("Cannot update secondary CA flag in state %q", c.state)
	}

	return nil
}

// configuredSecondaryCA returns true if we have been initialized as a secondary datacenter's CA.
func (c *CAManager) configuredSecondaryCA() bool {
	c.stateLock.Lock()
	defer c.stateLock.Unlock()
	return c.actingSecondaryCA
}

func primaryUsesIntermediate(provider ca.Provider) bool {
	_, ok := provider.(ca.PrimaryUsesIntermediate)
	return ok
}

func (c *CAManager) isIntermediateUsedToSignLeaf() bool {
	if c.serverConf.Datacenter != c.serverConf.PrimaryDatacenter {
		return true
	}
	provider, _ := c.getCAProvider()
	return primaryUsesIntermediate(provider)
}
