/*
Copyright IBM Corp. 2017 All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package follower

import (
	"bytes"
	"sync"
	"sync/atomic"
	"time"

	"github.com/hyperledger/fabric-lib-go/bccsp"
	"github.com/hyperledger/fabric-lib-go/common/flogging"
	"github.com/hyperledger/fabric-protos-go-apiv2/common"
	"github.com/hyperledger/fabric/orderer/common/cluster"
	"github.com/hyperledger/fabric/orderer/common/types"
	"github.com/hyperledger/fabric/orderer/consensus"
	"github.com/hyperledger/fabric/protoutil"
	"github.com/pkg/errors"
	"google.golang.org/protobuf/proto"
)

// ErrChainStopped is returned when the chain is stopped during execution.
var ErrChainStopped = errors.New("chain stopped")

//go:generate counterfeiter -o mocks/ledger_resources.go -fake-name LedgerResources . LedgerResources

// LedgerResources defines some of the interfaces of ledger & config resources needed by the follower.Chain.
type LedgerResources interface {
	// ChannelID The channel ID.
	ChannelID() string

	// Block returns a block with the given number,
	// or nil if such a block doesn't exist.
	Block(number uint64) *common.Block

	// Height returns the number of blocks in the chain this channel is associated with.
	Height() uint64

	// Append appends a new block to the ledger in its raw form.
	Append(block *common.Block) error
}

// TimeAfter has the signature of time.After and allows tests to provide an alternative implementation to it.
type TimeAfter func(d time.Duration) <-chan time.Time

//go:generate counterfeiter -o mocks/block_puller_factory.go -fake-name BlockPullerFactory . BlockPullerFactory

// BlockPullerFactory creates a ChannelPuller on demand, and exposes a method to update the a block signature verifier
// linked to that ChannelPuller.
type BlockPullerFactory interface {
	BlockPuller(configBlock *common.Block, stopChannel chan struct{}) (ChannelPuller, error)
	UpdateVerifierFromConfigBlock(configBlock *common.Block) error
}

//go:generate counterfeiter -o mocks/chain_creator.go -fake-name ChainCreator . ChainCreator

// ChainCreator defines a function that creates a new consensus.Chain for this channel, to replace the current
// follower.Chain. This interface is meant to be implemented by the multichannel.Registrar.
type ChainCreator interface {
	SwitchFollowerToChain(chainName string) bool
}

//go:generate counterfeiter -o mocks/channel_participation_metrics_reporter.go -fake-name ChannelParticipationMetricsReporter . ChannelParticipationMetricsReporter

type ChannelParticipationMetricsReporter interface {
	ReportConsensusRelationAndStatusMetrics(channelID string, relation types.ConsensusRelation, status types.Status)
}

// Chain implements a component that allows the orderer to follow a specific channel when is not a cluster member,
// that is, be a "follower" of the cluster. It also allows the orderer to perform "onboarding" for
// channels it is joining as a member, with a join-block.
//
// When an orderer is following a channel, it means that the current orderer is not a member of the consenters set
// of the channel, and is only pulling blocks from other orderers. In this mode, the follower is inspecting config
// blocks as they are pulled and if it discovers that it was introduced into the consenters set, it will trigger the
// creation of a regular etcdraft.Chain, that is, turn into a "member" of the cluster.
//
// A follower is also used to onboard a channel when joining as a member with a join-block that has number >0. In this
// mode the follower will pull blocks up until join-block.number, and then will trigger the creation of a regular
// etcdraft.Chain.
//
// The follower is started in one of two ways: 1) following an API Join request with a join-block that has
// block number >0, or 2) when the orderer was a cluster member (i.e. was running a etcdraft.Chain) and was removed
// from the consenters set.
//
// The follower is in status "onboarding" when it pulls blocks below the join-block number, or "active" when it
// pulls blocks equal or above the join-block number.
//
// The follower return clusterRelation "member" when the join-block indicates the orderer is in the consenters set,
// i.e. the follower is performing onboarding for an etcdraft.Chain. Otherwise, the follower return clusterRelation
// "follower".
type Chain struct {
	mutex             sync.Mutex    // Protects the start/stop flags & channels, consensusRelation & status. All the rest are immutable or accessed only by the go-routine.
	started           bool          // Start once.
	stopped           bool          // Stop once.
	stopChan          chan struct{} // A 'closer' signals the go-routine to stop by closing this channel.
	doneChan          chan struct{} // The go-routine signals the 'closer' that it is done by closing this channel.
	consensusRelation types.ConsensusRelation
	status            types.Status
	removingExternal  atomic.Int32 // External channel removal once.

	ledgerResources  LedgerResources            // ledger & config resources
	clusterConsenter consensus.ClusterConsenter // detects whether a block indicates channel membership
	options          Options
	logger           *flogging.FabricLogger
	timeAfter        TimeAfter // time.After by default, or an alternative from Options.

	joinBlock   *common.Block // The join-block the follower was started with.
	lastConfig  *common.Block // The last config block from the ledger. Accessed only by the go-routine.
	firstHeight uint64        // The first ledger height

	// Creates a block puller on demand, and allows the update of the block signature verifier with each incoming
	// config block.
	blockPullerFactory BlockPullerFactory
	// A block puller instance, created either from the join-block or last-config-block. When pulling blocks using
	// the last-config-block, the endpoints are updated with each incoming config block.
	blockPuller ChannelPuller

	// Creates a new consensus.Chain for this channel, to replace the current follower.Chain.
	chainCreator ChainCreator

	cryptoProvider bccsp.BCCSP // Cryptographic services

	channelParticipationMetricsReporter ChannelParticipationMetricsReporter
}

// NewChain constructs a follower.Chain object.
func NewChain(
	ledgerResources LedgerResources,
	clusterConsenter consensus.ClusterConsenter,
	joinBlock *common.Block,
	options Options,
	blockPullerFactory BlockPullerFactory,
	chainCreator ChainCreator,
	cryptoProvider bccsp.BCCSP,
	channelParticipationMetricsReporter ChannelParticipationMetricsReporter,
) (*Chain, error) {
	options.applyDefaults()

	chain := &Chain{
		stopChan:                            make(chan struct{}),
		doneChan:                            make(chan struct{}),
		consensusRelation:                   types.ConsensusRelationFollower,
		status:                              types.StatusOnBoarding,
		ledgerResources:                     ledgerResources,
		clusterConsenter:                    clusterConsenter,
		joinBlock:                           joinBlock,
		firstHeight:                         ledgerResources.Height(),
		options:                             options,
		logger:                              options.Logger.With("channel", ledgerResources.ChannelID()),
		timeAfter:                           options.TimeAfter,
		blockPullerFactory:                  blockPullerFactory,
		chainCreator:                        chainCreator,
		cryptoProvider:                      cryptoProvider,
		channelParticipationMetricsReporter: channelParticipationMetricsReporter,
	}

	if ledgerResources.Height() > 0 {
		if err := chain.loadLastConfig(); err != nil {
			return nil, err
		}
		if err := blockPullerFactory.UpdateVerifierFromConfigBlock(chain.lastConfig); err != nil {
			return nil, err
		}
	}

	if joinBlock == nil {
		chain.status = types.StatusActive
		if isMem, _ := chain.clusterConsenter.IsChannelMember(chain.lastConfig); isMem {
			chain.consensusRelation = types.ConsensusRelationConsenter
		}

		chain.logger.Infof("Created with a nil join-block, ledger height: %d", chain.firstHeight)
	} else {
		if joinBlock.Header == nil {
			return nil, errors.New("block header is nil")
		}
		if joinBlock.Data == nil {
			return nil, errors.New("block data is nil")
		}

		// Check the block puller creation function once before we start the follower. This ensures we can extract
		// the endpoints from the join-block.
		puller, err := blockPullerFactory.BlockPuller(joinBlock, nil)
		if err != nil {
			return nil, errors.WithMessage(err, "error creating a block puller from join-block")
		}
		puller.Close()

		if chain.joinBlock.Header.Number < chain.ledgerResources.Height() {
			chain.status = types.StatusActive
		}
		if isMem, _ := chain.clusterConsenter.IsChannelMember(chain.joinBlock); isMem {
			chain.consensusRelation = types.ConsensusRelationConsenter
		}

		chain.logger.Infof("Created with join-block number: %d, ledger height: %d", joinBlock.Header.Number, chain.firstHeight)
	}

	chain.logger.Debugf("Options are: %v", chain.options)

	chain.channelParticipationMetricsReporter.ReportConsensusRelationAndStatusMetrics(ledgerResources.ChannelID(), chain.consensusRelation, chain.status)

	return chain, nil
}

func (c *Chain) Start() {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	if c.started || c.stopped {
		c.logger.Debugf("Not starting because: started=%v, stopped=%v", c.started, c.stopped)
		return
	}

	c.started = true

	go c.run()

	c.logger.Info("Started")
}

// Halt signals the Chain to stop and waits for the internal go-routine to exit.
func (c *Chain) Halt() {
	c.removingExternal.Store(1)
	c.halt()
	<-c.doneChan
}

func (c *Chain) halt() {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	if c.stopped {
		c.logger.Debug("Already stopped")
		return
	}
	c.stopped = true
	close(c.stopChan)
	c.logger.Info("Stopped")
}

// StatusReport returns the ConsensusRelation & Status.
func (c *Chain) StatusReport() (types.ConsensusRelation, types.Status) {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	return c.consensusRelation, c.status
}

func (c *Chain) setStatus(status types.Status) {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	c.status = status

	c.channelParticipationMetricsReporter.ReportConsensusRelationAndStatusMetrics(c.ledgerResources.ChannelID(), c.consensusRelation, c.status)
}

func (c *Chain) setConsensusRelation(clusterRelation types.ConsensusRelation) {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	c.consensusRelation = clusterRelation

	c.channelParticipationMetricsReporter.ReportConsensusRelationAndStatusMetrics(c.ledgerResources.ChannelID(), c.consensusRelation, c.status)
}

func (c *Chain) Height() uint64 {
	return c.ledgerResources.Height()
}

func (c *Chain) IsRunning() bool {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	if c.started {
		select {
		case <-c.doneChan:
			return false
		default:
			return true
		}
	}

	return false
}

func (c *Chain) run() {
	c.logger.Debug("The follower.Chain puller goroutine is starting")

	defer func() {
		close(c.doneChan)
		c.logger.Debug("The follower.Chain puller goroutine is exiting")
	}()

	if err := c.pull(); err != nil {
		c.logger.Warnf("Pull failed, follower chain stopped, error: %s", err)
		// TODO set the status to StatusError (see FAB-18106)
	}
}

func (c *Chain) increaseRetryInterval(retryInterval *time.Duration, upperLimit time.Duration) {
	if *retryInterval == upperLimit {
		return
	}
	// assuming this will never overflow int64, as upperLimit cannot be over MaxInt64/2
	*retryInterval = time.Duration(1.5 * float64(*retryInterval))
	if *retryInterval > upperLimit {
		*retryInterval = upperLimit
	}
	c.logger.Debugf("retry interval increased to: %v", *retryInterval)
}

func (c *Chain) resetRetryInterval(retryInterval *time.Duration, lowerLimit time.Duration) {
	if *retryInterval == lowerLimit {
		return
	}
	*retryInterval = lowerLimit
	c.logger.Debugf("retry interval reset to: %v", *retryInterval)
}

func (c *Chain) decreaseRetryInterval(retryInterval *time.Duration, lowerLimit time.Duration) {
	if *retryInterval == lowerLimit {
		return
	}

	*retryInterval = *retryInterval - lowerLimit
	if *retryInterval < lowerLimit {
		*retryInterval = lowerLimit
	}
	c.logger.Debugf("retry interval decreased to: %v", *retryInterval)
}

// pull blocks from other orderers until a config block indicates the orderer has become a member of the cluster.
// When the follower.Chain's job is done, this method halts, triggers the creation of a new consensus.Chain,
// and returns nil. The method returns an error only when the chain is stopped or due to unrecoverable errors.
func (c *Chain) pull() error {
	var err error
	if c.joinBlock != nil {
		err = c.pullUpToJoin()
		if err != nil {
			return errors.WithMessage(err, "failed to pull up to join block")
		}
		c.logger.Info("Onboarding finished successfully, pulled blocks up to join-block")
	}

	err = c.pullAfterJoin()
	if err != nil {
		return errors.WithMessage(err, "failed to pull after join block")
	}

	// Trigger creation of a new consensus.Chain.
	c.logger.Info("Block pulling finished successfully, going to switch from follower to a consensus.Chain")
	c.halt()

	ticker := time.NewTicker(50 * time.Millisecond)
	defer ticker.Stop()

	// SwitchFollowerToChain is trying to take over the mutex.
	// If it fails, false is returned.
	// Perhaps at this moment the command "remove channel" is being executed,
	// let's check it `c.removingExternal.Load() == 1`. If so, leave the loop.
	// If SwitchFollowerToChain returns true, we will also leave the loop.
	// In other cases, we need to repeat the loop.
	for range ticker.C {
		if isExec := c.chainCreator.SwitchFollowerToChain(c.ledgerResources.ChannelID()); isExec ||
			c.removingExternal.Load() == 1 {
			break
		}
	}

	return nil
}

// pullUpToJoin pulls blocks up to the join-block height without inspecting membership on fetched config blocks.
// It checks whether the chain was stopped between blocks.
func (c *Chain) pullUpToJoin() error {
	targetHeight := c.joinBlock.Header.Number + 1
	if c.ledgerResources.Height() >= targetHeight {
		c.logger.Infof("Target height according to join block (%d) is <= to our ledger height (%d), no need to pull up to join block",
			targetHeight, c.ledgerResources.Height())
		return nil
	}

	var err error
	// Block puller created with endpoints from the join-block.
	c.blockPuller, err = c.blockPullerFactory.BlockPuller(c.joinBlock, c.stopChan)
	if err != nil { // This should never happen since we check the join-block before we start.
		return errors.WithMessagef(err, "error creating block puller")
	}
	defer c.blockPuller.Close()
	// Since we created the block-puller with the join-block, do not update the endpoints from the
	// config blocks that precede it.
	err = c.pullUntilLatestWithRetry(targetHeight, false)
	if err != nil {
		return err
	}

	c.logger.Infof("Pulled blocks from %d until %d", c.firstHeight, targetHeight-1)
	return nil
}

// pullAfterJoin pulls blocks continuously, inspecting the fetched config
// blocks for membership. On every config block, it renews the BlockPuller,
// to take in the new configuration. It will exit with 'nil' if it detects
// a config block that indicates the orderer is a member of the cluster. It
// checks whether the chain was stopped between blocks.
func (c *Chain) pullAfterJoin() error {
	c.logger.Infof("Pulling after join")
	defer c.logger.Infof("Pulled after join")
	c.setStatus(types.StatusActive)

	err := c.loadLastConfig()
	if err != nil {
		return errors.WithMessage(err, "failed to load last config block")
	}

	c.blockPuller, err = c.blockPullerFactory.BlockPuller(c.lastConfig, c.stopChan)
	if err != nil {
		return errors.WithMessage(err, "error creating block puller")
	}
	defer c.blockPuller.Close()

	heightPollInterval := c.options.HeightPollMinInterval
	for {
		// Check membership
		isMember, errMem := c.clusterConsenter.IsChannelMember(c.lastConfig)
		if errMem != nil {
			return errors.WithMessage(err, "failed to determine channel membership from last config")
		}
		if isMember {
			c.setConsensusRelation(types.ConsensusRelationConsenter)
			return nil
		}

		// Poll for latest network height to advance beyond ledger height.
		var latestNetworkHeight uint64
	heightPollLoop:
		for {
			endpoint, networkHeight, errHeight := cluster.LatestHeightAndEndpoint(c.blockPuller)
			if errHeight != nil {
				c.logger.Errorf("Failed to get latest height and endpoint, error: %s", errHeight)
			} else {
				c.logger.Infof("Orderer endpoint %s has the biggest ledger height: %d", endpoint, networkHeight)
			}

			if networkHeight > c.ledgerResources.Height() {
				// On success, slowly decrease the polling interval
				c.decreaseRetryInterval(&heightPollInterval, c.options.HeightPollMinInterval)
				latestNetworkHeight = networkHeight
				break heightPollLoop
			}

			c.logger.Infof("My height: %d, latest network height: %d; going to wait %v for latest height to grow",
				c.ledgerResources.Height(), networkHeight, heightPollInterval)
			select {
			case <-c.stopChan:
				c.logger.Debug("Received a stop signal")
				return ErrChainStopped
			case <-c.timeAfter(heightPollInterval):
				// Exponential back-off, to avoid calling LatestHeightAndEndpoint too often.
				c.increaseRetryInterval(&heightPollInterval, c.options.HeightPollMaxInterval)
			}
		}

		// Pull to latest height or chain stop signal
		err = c.pullUntilLatestWithRetry(latestNetworkHeight, true)
		if err != nil {
			return err
		}
	}
}

// pullUntilLatestWithRetry is given a target-height and exits without an error when it reaches that target.
// It return with an error only if the chain is stopped.
// On internal pull errors it employs exponential back-off and retries.
// When parameter updateEndpoints is true, the block-puller's endpoints are updated with every incoming config.
func (c *Chain) pullUntilLatestWithRetry(latestNetworkHeight uint64, updateEndpoints bool) error {
	retryInterval := c.options.PullRetryMinInterval
	for {
		numPulled, errPull := c.pullUntilTarget(latestNetworkHeight, updateEndpoints)
		if numPulled > 0 {
			c.resetRetryInterval(&retryInterval, c.options.PullRetryMinInterval) // On any progress, reset retry interval.
		}
		if errPull == nil {
			c.logger.Debugf("Pulled %d blocks until latest network height: %d", numPulled, latestNetworkHeight)
			break
		}

		c.logger.Debugf("Error while trying to pull to latest height: %d; going to try again in %v; error: %s",
			latestNetworkHeight, retryInterval, errPull)

		select {
		case <-c.stopChan:
			c.logger.Debug("Received a stop signal")
			return ErrChainStopped
		case <-c.timeAfter(retryInterval):
			// Exponential back-off on successive errors w/o progress.
			c.increaseRetryInterval(&retryInterval, c.options.PullRetryMaxInterval)
		}
	}

	return nil
}

// pullUntilTarget is given a target-height and exits without an error when it reaches that target.
// It may return with an error before the target, always returning the number of blocks pulled.
// When parameter updateEndpoints is true, the block-puller's endpoints are updated with every incoming config.
// The block-puller-factory which holds the block signature verifier is updated on every incoming config.
func (c *Chain) pullUntilTarget(targetHeight uint64, updateEndpoints bool) (uint64, error) {
	firstBlockToPull := c.ledgerResources.Height()
	if firstBlockToPull >= targetHeight {
		c.logger.Debugf("Target height (%d) is <= to our ledger height (%d), skipping pulling", targetHeight, firstBlockToPull)
		return 0, nil
	}

	var actualPrevHash []byte
	// Initialize the actual previous hash
	if firstBlockToPull > 0 {
		prevBlock := c.ledgerResources.Block(firstBlockToPull - 1)
		if prevBlock == nil {
			return 0, errors.Errorf("cannot retrieve previous block %d", firstBlockToPull-1)
		}
		actualPrevHash = protoutil.BlockHeaderHash(prevBlock.Header)
	}

	// Pull until the latest height
	for seq := firstBlockToPull; seq < targetHeight; seq++ {
		n := seq - firstBlockToPull
		select {
		case <-c.stopChan:
			c.logger.Debug("Received a stop signal")
			return n, ErrChainStopped
		default:
			nextBlock := c.blockPuller.PullBlock(seq)
			if nextBlock == nil {
				return n, errors.WithMessagef(cluster.ErrRetryCountExhausted, "failed to pull block %d", seq)
			}

			reportedPrevHash := nextBlock.Header.PreviousHash
			if (nextBlock.Header.Number > 0) && !bytes.Equal(reportedPrevHash, actualPrevHash) {
				return n, errors.Errorf("block header previous hash mismatch on sequence %d, expected %x, got %x",
					nextBlock.Header.Number, actualPrevHash, reportedPrevHash)
			}

			if c.joinBlock != nil && c.joinBlock.Header.Number == nextBlock.Header.Number {
				// We don't need to verify the block.Data because we verify the join-block's DataHash against the
				// hash(join-block.Data) when we verify it during the `Join` REST API call
				if !proto.Equal(nextBlock.Header, c.joinBlock.Header) {
					c.logger.Errorf("Block header mismatch between the block we pulled and the block we joined with, sequence %d", c.joinBlock.Header.Number)
					return n, errors.Errorf("block header mismatch between the block we pulled and the block we joined with, sequence %d", c.joinBlock.Header.Number)
				}
			}

			actualPrevHash = protoutil.BlockHeaderHash(nextBlock.Header)
			if err := c.ledgerResources.Append(nextBlock); err != nil {
				return n, errors.WithMessagef(err, "failed to append block %d to the ledger", nextBlock.Header.Number)
			}

			if protoutil.IsConfigBlock(nextBlock) {
				c.logger.Debugf("Pulled blocks from %d to %d, last block is config", firstBlockToPull, nextBlock.Header.Number)
				c.lastConfig = nextBlock
				if err := c.blockPullerFactory.UpdateVerifierFromConfigBlock(nextBlock); err != nil {
					return n, errors.WithMessagef(err, "failed to update verifier from last config,  block number: %d", nextBlock.Header.Number)
				}
				if updateEndpoints {
					endpoints, err := cluster.EndpointconfigFromConfigBlock(nextBlock, c.cryptoProvider)
					if err != nil {
						return n, errors.WithMessagef(err, "failed to extract endpoints from last config,  block number: %d", nextBlock.Header.Number)
					}
					c.blockPuller.UpdateEndpoints(endpoints)
				}
			}
		}
	}
	c.logger.Debugf("Pulled blocks from %d to %d", firstBlockToPull, targetHeight)
	return targetHeight - firstBlockToPull, nil
}

func (c *Chain) loadLastConfig() error {
	height := c.ledgerResources.Height()
	if height == 0 {
		return errors.New("ledger is empty")
	}
	lastBlock := c.ledgerResources.Block(height - 1)
	index, err := protoutil.GetLastConfigIndexFromBlock(lastBlock)
	if err != nil {
		return errors.WithMessage(err, "chain does have appropriately encoded last config in its latest block")
	}
	lastConfig := c.ledgerResources.Block(index)
	if lastConfig == nil {
		return errors.Errorf("could not retrieve config block from index %d", index)
	}
	c.lastConfig = lastConfig
	return nil
}
