using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.BuilderExtensions;
using NBitcoin.Crypto;
using NBitcoin.Protocol;
using Stratis.Bitcoin.Base;
using Stratis.Bitcoin.Consensus;
using Stratis.Bitcoin.Features.Consensus;
using Stratis.Bitcoin.Features.Consensus.CoinViews;
using Stratis.Bitcoin.Features.Consensus.Interfaces;
using Stratis.Bitcoin.Features.Consensus.Rules.CommonRules;
using Stratis.Bitcoin.Features.MemoryPool;
using Stratis.Bitcoin.Features.MemoryPool.Interfaces;
using Stratis.Bitcoin.Features.Miner.Interfaces;
using Stratis.Bitcoin.Features.Wallet;
using Stratis.Bitcoin.Features.Wallet.Interfaces;
using Stratis.Bitcoin.Interfaces;
using Stratis.Bitcoin.Mining;
using Stratis.Bitcoin.Primitives;
using Stratis.Bitcoin.Utilities;

namespace Stratis.Bitcoin.Features.Miner.Staking
{
    /// <summary>
    /// <see cref="PosMinting"/> is used in order to generate new blocks. It involves a sort of lottery, similar to proof-of-work,
    /// but the chances of winning this lottery is proportional to how many coins you are staking, not on hashing power.
    /// </summary>
    /// <remarks>
    /// Staking is attempted only if the node is fully synchronized and connected to the network.
    /// If not it will wait till node is synced. Only transactions that were confirmed at least
    /// <see cref="PosConsensusOptions.GetStakeMinConfirmations(int, Network)"/> blocks ago are eligible for staking.
    /// <para>
    /// The overall process for "attempting" to mine a PoS block looks like this:
    /// <list type="number">
    /// <item>Create new block with transactions from mempool.</item>
    /// <item>Get UTXOs that can participate in staking (have suitable depth).</item>
    /// <item>Split these UTXOs in subsets and create tasks processing each subset to allow for parallel processing.</item>
    /// <item>Each of the tasks mentioned above will try to find a solution for proof of stake target. This is done by creating a coinstake
    /// transaction with each of the available UTXOs combined with all valid unix timestamps that were not checked.
    /// Those timestamps are within a time interval from now to now - searchInterval seconds. Only timestamps that are divisible by
    /// <c><see cref="BlockHeaderPosContextualRule.StakeTimestampMask"/> + 1</c> are valid candidates (this is done to decrease granularity of timestamps).
    /// Search interval is a length of an unexplored block time space in seconds.
    /// Task calculates the kernel's hash (kernel is the first input in the coinstake transaction) using the next formula:
    /// <c>hash(stakeModifierV2 + stakingCoins.Time + prevout.Hash + prevout.N + transactionTime)</c>.
    /// Then it calculates staking target using the next formula: <c>block difficulty * UTXO value</c>.
    /// We compare kernel's hash against the staking target, if it's greater then we met the criteria and kernel is found.
    /// So the more coins we stake the higher the staking target and so the higher the chance to meet the criteria.</item>
    /// <item>In case kernel is found we add a coinstake transaction, sign the block and add it to the chain.</item>
    /// </list>
    /// </para>
    /// <para>
    /// Coinstake transaction invalidates previous inputs and spends the inputs to new outputs with the additional stake reward.
    /// </para>
    /// <para>
    /// The purpose of stake modifier is to prevent a UTXO owner from computing future proof-of-stake
    /// generated by this UTXO at the time of transaction confirmation. As described above, the stake modifier
    /// is included in the hash that must meet the difficulty target. As the stake modifier changes with each block
    /// and the new value depends on the kernel, it is hard to predict its value in the future.
    /// </para>
    /// </remarks>
    public class PosMinting : IPosMinting
    {

        /// <summary>
        /// Indicates the current state: idle, staking requested, staking in progress and stop staking requested.
        /// </summary>
        public enum CurrentState
        {
            Idle = 0,
            StakingRequested = 1,
            StakingInProgress = 2,
            StopStakingRequested = 3
        }

        /// <summary>The maximum allowed size for a serialized block, in bytes (network rule).</summary>
        public const int MaxBlockSize = 1000000;

        ///<summary>The maximum size for mined blocks.</summary>
        public const int MaxBlockSizeGen = MaxBlockSize / 2;

        /// <summary>Builder that creates a proof-of-stake block template.</summary>
        private readonly IBlockProvider blockProvider;

        /// <summary><c>true</c> if coinstake transaction splits the coin and generates extra UTXO
        /// to prevent halting chain; <c>false</c> to disable coinstake splitting.</summary>
        public readonly bool CoinstakeSplitEnabled;

        /// <summary> If <see cref="CoinstakeSplitEnabled"/> is set, the coinstake will be split if
        /// the number of non-empty UTXOs in the wallet is lower than the required coin age for staking plus 1,
        /// multiplied by this value. See <see cref="GetSplitStake(int, ChainedHeader)"/>.</summary>
        public const int CoinstakeSplitLimitMultiplier = 3;

        /// <summary>Number of UTXO descriptions that a single worker's task will process.</summary>
        /// <remarks>To achieve a good level of parallelism, this should be low enough so that CPU threads are used,
        /// but high enough to compensate for tasks' overhead.</remarks>
        protected const int UtxoStakeDescriptionsPerCoinstakeWorker = 25;

        /// <summary>Consumes manager class.</summary>
        protected readonly IConsensusManager consensusManager;

        /// <summary>Thread safe access to the best chain of block headers (that the node is aware of) from genesis.</summary>
        private readonly ConcurrentChain chain;

        /// <summary>Specification of the network the node runs on - regtest/testnet/mainnet.</summary>
        protected readonly Network network;

        /// <summary>Provides date time functionality.</summary>
        private readonly IDateTimeProvider dateTimeProvider;

        /// <summary>Global application life cycle control - triggers when application shuts down.</summary>
        private readonly INodeLifetime nodeLifetime;

        /// <summary>Consensus' view of UTXO set.</summary>
        private readonly ICoinView coinView;

        /// <summary>Database of stake related data for the current blockchain.</summary>
        private readonly IStakeChain stakeChain;

        /// <summary>Provides functionality for checking validity of PoS blocks.</summary>
        private readonly IStakeValidator stakeValidator;

        /// <summary>Factory for creating background async loop tasks.</summary>
        private readonly IAsyncLoopFactory asyncLoopFactory;

        /// <summary>A manager providing operations on wallets.</summary>
        protected readonly IWalletManager walletManager;

        /// <summary>Factory for creating loggers.</summary>
        protected readonly ILoggerFactory loggerFactory;

        /// <summary>Instance logger.</summary>
        protected readonly ILogger logger;

        /// <summary>Loop in which the node attempts to generate new POS blocks by staking coins from its wallet.</summary>
        private IAsyncLoop stakingLoop;

        /// <summary>A flag that indicates the current state based on the <see cref="CurrentState"/> enum.</summary>
        private int currentState;

        /// <summary>
        /// We don't stake coins that are smaller than 0.1 in order to save on CPU as these have a very small chance to be used
        /// to generate a block anyway.
        /// <seealso cref="https://github.com/stratisproject/StratisBitcoinFullNode/issues/1180"/>
        /// </summary>
        public readonly ulong MinimumStakingCoinValue;

        /// <summary>When splitting a big utxo, this is the number of smaller utxos we divide it into.</summary>
        internal const int SplitFactor = 8;

        /// <summary>Minimum value of a split utxo we are aiming for (after splitting it into <see cref="SplitFactor" /> equal parts).</summary>
        private readonly ulong MinimumSplitCoinValue;

        /// <summary>
        /// Target reserved balance that will not participate in staking.
        /// It is possible that less than this amount will be reserved.
        /// </summary>
        protected Money targetReserveBalance;

        /// <summary>Time in milliseconds between attempts to generate PoS blocks.</summary>
        private readonly int minerSleep;

        /// <summary>Time in milliseconds between attempts to generate PoS blocks, when the system time is out of sync.</summary>
        private readonly int systemTimeOutOfSyncSleep;

        /// <summary>A lock for managing asynchronous access to memory pool.</summary>
        protected readonly MempoolSchedulerLock mempoolLock;

        /// <summary>Memory pool of pending transactions.</summary>
        protected readonly ITxMempool mempool;

        /// <summary>Script types that can participate in staking.</summary>
        public Dictionary<string, ScriptTemplate> ValidStakingTemplates;

        /// <summary>Information about node's staking for RPC "getstakinginfo" command.</summary>
        /// <remarks>This object does not need a synchronized access because there is no execution logic
        /// that depends on the reported information.</remarks>
        protected Models.GetStakingInfoModel rpcGetStakingInfoModel;

        /// <summary>Estimation of the total staking weight of all nodes on the network.</summary>
        protected long networkWeight;

        /// <summary>
        /// Timestamp of the last attempt to search for POS solution.
        /// <para>
        /// It is used to prevent searching for solutions that were already proved wrong in the past.
        /// If there is no new block since last time we searched for the solution, it does not make
        /// sense to try timestamps earlier than this value.
        /// </para>
        /// </summary>
        private long lastCoinStakeSearchTime;

        /// <summary>
        /// Hash of the block headers of the block that was at the tip of the chain during our last
        /// search for POS solution.
        /// <para>
        /// It is used to prevent searching for solutions that were already proved wrong in the past.
        /// If the tip changes, <see cref="lastCoinStakeSearchTime"/> is set to the new tip's header hash.
        /// </para>
        /// </summary>
        private uint256 lastCoinStakeSearchPrevBlockHash;

        /// <summary>
        /// A cancellation token source that can cancel the staking processes and is linked to the <see cref="INodeLifetime.ApplicationStopping"/>.
        /// </summary>
        private CancellationTokenSource stakeCancellationTokenSource;

        /// <summary>Provider of IBD state.</summary>
        private readonly IInitialBlockDownloadState initialBlockDownloadState;

        /// <summary>State of time synchronization feature that stores collected data samples.</summary>
        private readonly ITimeSyncBehaviorState timeSyncBehaviorState;

        public PosMinting(
            IBlockProvider blockProvider,
            IConsensusManager consensusManager,
            ConcurrentChain chain,
            Network network,
            IDateTimeProvider dateTimeProvider,
            IInitialBlockDownloadState initialBlockDownloadState,
            INodeLifetime nodeLifetime,
            ICoinView coinView,
            IStakeChain stakeChain,
            IStakeValidator stakeValidator,
            MempoolSchedulerLock mempoolLock,
            ITxMempool mempool,
            IWalletManager walletManager,
            IAsyncLoopFactory asyncLoopFactory,
            ITimeSyncBehaviorState timeSyncBehaviorState,
            ILoggerFactory loggerFactory,
            MinerSettings minerSettings)
        {
            this.blockProvider = blockProvider;
            this.consensusManager = consensusManager;
            this.chain = chain;
            this.network = network;
            this.dateTimeProvider = dateTimeProvider;
            this.initialBlockDownloadState = initialBlockDownloadState;
            this.nodeLifetime = nodeLifetime;
            this.coinView = coinView;
            this.stakeChain = stakeChain;
            this.stakeValidator = stakeValidator;
            this.mempoolLock = mempoolLock;
            this.mempool = mempool;
            this.asyncLoopFactory = asyncLoopFactory;
            this.walletManager = walletManager;
            this.timeSyncBehaviorState = timeSyncBehaviorState;
            this.loggerFactory = loggerFactory;
            this.logger = loggerFactory.CreateLogger(this.GetType().FullName);

            this.minerSleep = 500; // GetArg("-minersleep", 500);
            this.systemTimeOutOfSyncSleep = 7000;
            this.lastCoinStakeSearchTime = this.dateTimeProvider.GetAdjustedTimeAsUnixTimestamp();
            this.lastCoinStakeSearchPrevBlockHash = 0;
            this.targetReserveBalance = 0; // TODO:settings.targetReserveBalance
            this.currentState = (int)CurrentState.Idle;

            this.rpcGetStakingInfoModel = new Models.GetStakingInfoModel();

            this.CoinstakeSplitEnabled = minerSettings.EnableCoinStakeSplitting;
            this.MinimumStakingCoinValue = minerSettings.MinimumStakingCoinValue;
            this.MinimumSplitCoinValue = minerSettings.MinimumSplitCoinValue;
            this.ValidStakingTemplates = walletManager.GetValidStakingTemplates();
        }

        /// <inheritdoc/>
        public void Stake(WalletSecret walletSecret)
        {
            Guard.NotNull(walletSecret, nameof(walletSecret));

            if (Interlocked.CompareExchange(ref this.currentState, (int)CurrentState.StakingRequested, (int)CurrentState.Idle) != (int)CurrentState.Idle)
            {
                this.logger.LogTrace("(-)[NOT_IDLE]");
                return;
            }

            this.rpcGetStakingInfoModel.Enabled = true;
            this.stakeCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(new[] { this.nodeLifetime.ApplicationStopping });

            this.stakingLoop = this.asyncLoopFactory.Run("PosMining.Stake", async token =>
            {
                try
                {
                    await this.GenerateBlocksAsync(walletSecret, this.stakeCancellationTokenSource.Token)
                        .ConfigureAwait(false);
                }
                catch (OperationCanceledException)
                {
                    // Application stopping, nothing to do as the loop will be stopped.
                }
                catch (MinerException me)
                {
                    // Miner exceptions should be ignored. It means that the miner
                    // possibly mined a block that was not accepted by peers or is even invalid,
                    // but it should not halted the staking operation.
                    this.logger.LogDebug("Miner exception occurred in miner loop: {0}", me.ToString());
                    this.rpcGetStakingInfoModel.Errors = me.Message;
                }
                catch (ConsensusErrorException cee)
                {
                    // All consensus exceptions should be ignored. It means that the miner
                    // run into problems while constructing block or verifying it
                    // but it should not halted the staking operation.
                    this.logger.LogDebug("Consensus error exception occurred in miner loop: {0}", cee.ToString());
                    this.rpcGetStakingInfoModel.Errors = cee.Message;
                }
                catch (Exception ex)
                {
                    this.logger.LogError("Exception: {0}", ex);
                    this.logger.LogTrace("(-)[UNHANDLED_EXCEPTION]");
                    throw;
                }
            },
            this.stakeCancellationTokenSource.Token,
            repeatEvery: TimeSpan.FromMilliseconds(this.minerSleep),
            startAfter: TimeSpans.Second);

            Interlocked.CompareExchange(ref this.currentState, (int)CurrentState.StakingInProgress, (int)CurrentState.StakingRequested);
        }

        /// <inheritdoc/>
        public void StopStake()
        {
            if (Interlocked.CompareExchange(ref this.currentState, (int)CurrentState.StopStakingRequested, (int)CurrentState.StakingInProgress) != (int)CurrentState.StakingInProgress)
            {
                this.logger.LogTrace("(-)[STAKING_NOT_IN_PROGRESS]");
                return;
            }

            this.stakeCancellationTokenSource?.Cancel();
            this.logger.LogTrace("Disposing staking loop.");
            this.stakingLoop?.Dispose();
            this.stakingLoop = null;
            this.stakeCancellationTokenSource?.Dispose();
            this.stakeCancellationTokenSource = null;
            this.rpcGetStakingInfoModel.StopStaking();

            Interlocked.CompareExchange(ref this.currentState, (int)CurrentState.Idle, (int)CurrentState.StopStakingRequested);
        }

        /// <inheritdoc/>
        public async Task GenerateBlocksAsync(WalletSecret walletSecret, CancellationToken cancellationToken)
        {
            Guard.NotNull(walletSecret, nameof(walletSecret));

            BlockTemplate blockTemplate = null;

            while (!cancellationToken.IsCancellationRequested)
            {
                // Prevent mining if the system time is not in sync with that of other members on the network.
                if (this.timeSyncBehaviorState.IsSystemTimeOutOfSync)
                {
                    this.logger.LogError("Staking cannot start, your system time does not match that of other nodes on the network." + Environment.NewLine
                                         + "Please adjust your system time and restart the node.");
                    await Task.Delay(TimeSpan.FromMilliseconds(this.systemTimeOutOfSyncSleep), cancellationToken).ConfigureAwait(false);
                    continue;
                }

                // Prevent staking if not in initial block download.
                if (this.initialBlockDownloadState.IsInitialBlockDownload())
                {
                    this.logger.LogTrace("Waiting for synchronization before mining can be started.");

                    await Task.Delay(TimeSpan.FromMilliseconds(this.minerSleep), cancellationToken).ConfigureAwait(false);
                    continue;
                }

                ChainedHeader chainTip = this.consensusManager.Tip;

                if (this.lastCoinStakeSearchPrevBlockHash != chainTip.HashBlock)
                {
                    this.lastCoinStakeSearchPrevBlockHash = chainTip.HashBlock;
                    this.lastCoinStakeSearchTime = chainTip.Header.Time;
                    this.logger.LogTrace("New block '{0}' detected, setting last search time to its timestamp {1}.", chainTip, chainTip.Header.Time);

                    // Reset the template as the chain advanced.
                    blockTemplate = null;
                }

                uint coinstakeTimestamp = (uint)this.dateTimeProvider.GetAdjustedTimeAsUnixTimestamp() & ~PosConsensusOptions.StakeTimestampMask;
                if (coinstakeTimestamp <= this.lastCoinStakeSearchTime)
                {
                    this.logger.LogTrace("Current coinstake time {0} is not greater than last search timestamp {1}.", coinstakeTimestamp, this.lastCoinStakeSearchTime);
                    this.logger.LogTrace("(-)[NOTHING_TO_DO]");
                    return;
                }

                List<UtxoStakeDescription> utxoStakeDescriptions = await this.GetUtxoStakeDescriptionsAsync(walletSecret, cancellationToken).ConfigureAwait(false);

                blockTemplate = blockTemplate ?? this.blockProvider.BuildPosBlock(chainTip, new Script());
                var posBlock = (PosBlock)blockTemplate.Block;

                this.networkWeight = (long)this.GetNetworkWeight();
                this.rpcGetStakingInfoModel.CurrentBlockSize = posBlock.GetSerializedSize();
                this.rpcGetStakingInfoModel.CurrentBlockTx = posBlock.Transactions.Count();
                this.rpcGetStakingInfoModel.PooledTx = await this.mempoolLock.ReadAsync(() => this.mempool.MapTx.Count).ConfigureAwait(false);
                this.rpcGetStakingInfoModel.Difficulty = this.GetDifficulty(chainTip);
                this.rpcGetStakingInfoModel.NetStakeWeight = this.networkWeight;

                // Trying to create coinstake that satisfies the difficulty target, put it into a block and sign the block.
                if (await this.StakeAndSignBlockAsync(utxoStakeDescriptions, posBlock, chainTip, blockTemplate.TotalFee, coinstakeTimestamp).ConfigureAwait(false))
                {
                    this.logger.LogTrace("New POS block created and signed successfully.");
                    await this.CheckStakeAsync(posBlock, chainTip).ConfigureAwait(false);

                    blockTemplate = null;
                }
                else
                {
                    this.logger.LogTrace("{0} failed to create POS block, waiting {1} ms for next round.", nameof(this.StakeAndSignBlockAsync), this.minerSleep);
                    await Task.Delay(TimeSpan.FromMilliseconds(this.minerSleep), cancellationToken).ConfigureAwait(false);
                }
            }
        }

        internal async Task<List<UtxoStakeDescription>> GetUtxoStakeDescriptionsAsync(WalletSecret walletSecret, CancellationToken cancellationToken)
        {
            var utxoStakeDescriptions = new List<UtxoStakeDescription>();
            List<UnspentOutputReference> spendableTransactions = this.walletManager
                .GetSpendableTransactionsInWalletForStaking(walletSecret.WalletName, 1).ToList();

            FetchCoinsResponse fetchedCoinSet = await this.coinView.FetchCoinsAsync(spendableTransactions.Select(t => t.Transaction.Id).Distinct().ToArray(), cancellationToken).ConfigureAwait(false);

            foreach (UnspentOutputReference outputReference in spendableTransactions)
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    this.logger.LogTrace("(-)[CANCELLATION]");
                    throw new OperationCanceledException(cancellationToken);
                }

                UnspentOutputs coinSet = fetchedCoinSet.UnspentOutputs
                    .FirstOrDefault(f => f?.TransactionId == outputReference.Transaction.Id);
                if ((coinSet == null) || (outputReference.Transaction.Index >= coinSet.Outputs.Length))
                    continue;

                TxOut utxo = coinSet.Outputs[outputReference.Transaction.Index];
                if ((utxo == null) || (utxo.Value < MinimumStakingCoinValue))
                    continue;

                uint256 hashBlock = this.chain.GetBlock((int)coinSet.Height)?.HashBlock;
                if (hashBlock == null)
                    continue;

                var utxoStakeDescription = new UtxoStakeDescription
                {
                    TxOut = utxo,
                    OutPoint = new OutPoint(coinSet.TransactionId, outputReference.Transaction.Index),
                    Address = outputReference.Address,
                    HashBlock = hashBlock,
                    UtxoSet = coinSet,
                    Secret = walletSecret // Temporary.
                };
                utxoStakeDescriptions.Add(utxoStakeDescription);

                this.logger.LogTrace("UTXO '{0}' with value {1} might be available for staking.", utxoStakeDescription.OutPoint, utxo.Value);
            }

            this.logger.LogTrace("Wallet total staking balance is {0}.", new Money(utxoStakeDescriptions.Sum(d => d.TxOut.Value)));
            return utxoStakeDescriptions;
        }

        /// <summary>
        /// One a new block is staked, this method is used to verify that it
        /// is a valid block and if so, it will add it to the chain.
        /// </summary>
        /// <param name="block">The new block.</param>
        /// <param name="chainTip">Block that was considered as a chain tip when the block staking started.</param>
        private async Task CheckStakeAsync(Block block, ChainedHeader chainTip)
        {
            if (!BlockStake.IsProofOfStake(block))
            {
                this.logger.LogTrace("(-)[NOT_POS]");
                return;
            }

            // Verify hash target and signature of coinstake tx.
            BlockStake prevBlockStake = this.stakeChain.Get(chainTip.HashBlock);
            if (prevBlockStake == null)
            {
                this.logger.LogTrace("(-)[NO_PREV_STAKE]");
                ConsensusErrors.PrevStakeNull.Throw();
            }

            // Validate the block.
            ChainedHeader chainedHeader = await this.consensusManager.BlockMinedAsync(block).ConfigureAwait(false);

            if (chainedHeader == null)
            {
                this.logger.LogTrace("(-)[REORG-2]");
                return;
            }

            this.logger.LogInformation("==================================================================");
            this.logger.LogInformation("Found new POS block hash '{0}' at height {1}.", chainedHeader.HashBlock, chainedHeader.Height);
            this.logger.LogInformation("==================================================================");
        }

        /// <summary>
        /// Attempts to find a POS staking solution and if it succeeds, then it completes a block
        /// to be mined and signes it.
        /// </summary>
        /// <param name="utxoStakeDescriptions">List of UTXOs that are available in the wallet for staking.</param>
        /// <param name="block">Template of the block that we are trying to mine.</param>
        /// <param name="chainTip">Tip of the best chain.</param>
        /// <param name="fees">Transaction fees from the transactions included in the block if we mine it.</param>
        /// <param name="coinstakeTimestamp">Maximal timestamp of the coinstake transaction. The actual timestamp can be lower, but not higher.</param>
        /// <returns><c>true</c> if the function succeeds, <c>false</c> otherwise.</returns>
        private async Task<bool> StakeAndSignBlockAsync(List<UtxoStakeDescription> utxoStakeDescriptions, PosBlock block, ChainedHeader chainTip, long fees, uint coinstakeTimestamp)
        {
            // If we are trying to sign something except proof-of-stake block template.
            if (!block.Transactions[0].Outputs[0].IsEmpty)
            {
                this.logger.LogTrace("(-)[NO_POS_BLOCK]:false");
                return false;
            }

            // If we are trying to sign a complete proof-of-stake block.
            if (BlockStake.IsProofOfStake(block))
            {
                this.logger.LogTrace("(-)[ALREADY_DONE]:true");
                return true;
            }

            var coinstakeContext = new CoinstakeContext();
            coinstakeContext.CoinstakeTx = this.network.CreateTransaction();
            coinstakeContext.CoinstakeTx.Time = coinstakeTimestamp;

            // Search to current coinstake time.
            long searchTime = coinstakeContext.CoinstakeTx.Time;

            long searchInterval = searchTime - this.lastCoinStakeSearchTime;
            this.rpcGetStakingInfoModel.SearchInterval = (int)searchInterval;

            this.lastCoinStakeSearchTime = searchTime;
            this.logger.LogTrace("Search interval set to {0}, last coinstake search timestamp set to {1}.", searchInterval, this.lastCoinStakeSearchTime);

            if (await this.CreateCoinstakeAsync(utxoStakeDescriptions, block, chainTip, searchInterval, fees, coinstakeContext).ConfigureAwait(false))
            {
                uint minTimestamp = chainTip.Header.Time + 1;
                if (coinstakeContext.CoinstakeTx.Time >= minTimestamp)
                {
                    // Make sure coinstake would meet timestamp protocol
                    // as it would be the same as the block timestamp.
                    block.Transactions[0].Time = block.Header.Time = coinstakeContext.CoinstakeTx.Time;

                    // We have to make sure that we have no future timestamps in
                    // our transactions set.
                    for (int i = block.Transactions.Count - 1; i >= 0; i--)
                    {
                        if (block.Transactions[i].Time > block.Header.Time)
                        {
                            this.logger.LogTrace("Removing transaction with timestamp {0} as it is greater than coinstake transaction timestamp {1}.", block.Transactions[i].Time, block.Header.Time);
                            block.Transactions.Remove(block.Transactions[i]);
                        }
                    }

                    block.Transactions.Insert(1, coinstakeContext.CoinstakeTx);
                    block.UpdateMerkleRoot();

                    // Append a signature to our block.
                    ECDSASignature signature = coinstakeContext.Key.Sign(block.GetHash());

                    block.BlockSignature = new BlockSignature { Signature = signature.ToDER() };
                    return true;
                }
                else this.logger.LogTrace("Coinstake transaction created with too early timestamp {0}, minimal timestamp is {1}.", coinstakeContext.CoinstakeTx.Time, minTimestamp);
            }
            else this.logger.LogTrace("Unable to create coinstake transaction.");

            return false;
        }

        /// <inheritdoc/>
        public virtual async Task<bool> CreateCoinstakeAsync(List<UtxoStakeDescription> utxoStakeDescriptions, Block block, ChainedHeader chainTip, long searchInterval, long fees, CoinstakeContext coinstakeContext)
        {
            coinstakeContext.CoinstakeTx.Inputs.Clear();
            coinstakeContext.CoinstakeTx.Outputs.Clear();

            // Mark coinstake transaction.
            coinstakeContext.CoinstakeTx.Outputs.Add(new TxOut(Money.Zero, new Script()));

            long balance = (await this.GetMatureBalanceAsync(utxoStakeDescriptions).ConfigureAwait(false)).Satoshi;
            if (balance <= this.targetReserveBalance)
            {
                this.rpcGetStakingInfoModel.PauseStaking();

                this.logger.LogTrace("Total balance of available UTXOs is {0}, which is less than or equal to reserve balance {1}.", balance, this.targetReserveBalance);
                this.logger.LogTrace("(-)[BELOW_RESERVE]:false");
                return false;
            }

            // Select UTXOs with suitable depth.
            List<UtxoStakeDescription> stakingUtxoDescriptions = await this.GetUtxoStakeDescriptionsSuitableForStakingAsync(utxoStakeDescriptions, chainTip, coinstakeContext.CoinstakeTx.Time, balance - this.targetReserveBalance).ConfigureAwait(false);
            if (!stakingUtxoDescriptions.Any())
            {
                this.rpcGetStakingInfoModel.PauseStaking();

                this.logger.LogTrace("(-)[NO_SELECTION]:false");
                return false;
            }

            long ourWeight = stakingUtxoDescriptions.Sum(s => s.TxOut.Value);
            long expectedTime = StakeValidator.TargetSpacingSeconds * this.networkWeight / ourWeight;
            decimal ourPercent = this.networkWeight != 0 ? 100.0m * (decimal)ourWeight / (decimal)this.networkWeight : 0;

            this.logger.LogInformation("Node staking with {0} ({1:0.00} % of the network weight {2}), est. time to find new block is {3}.", new Money(ourWeight), ourPercent, new Money(this.networkWeight), TimeSpan.FromSeconds(expectedTime));

            this.rpcGetStakingInfoModel.ResumeStaking(ourWeight, expectedTime);

            long minimalAllowedTime = chainTip.Header.Time + 1;
            this.logger.LogTrace("Trying to find staking solution among {0} transactions, minimal allowed time is {1}, coinstake time is {2}.", stakingUtxoDescriptions.Count, minimalAllowedTime, coinstakeContext.CoinstakeTx.Time);

            // If the time after applying the mask is lower than minimal allowed time,
            // it is simply too early for us to mine, there can't be any valid solution.
            if ((coinstakeContext.CoinstakeTx.Time & ~PosConsensusOptions.StakeTimestampMask) < minimalAllowedTime)
            {
                this.logger.LogTrace("(-)[TOO_EARLY_TIME_AFTER_LAST_BLOCK]:false");
                return false;
            }

            // Create worker tasks that will look for kernel.
            // Run task in parallel using the default task scheduler.
            int coinIndex = 0;
            int workerCount = (stakingUtxoDescriptions.Count + UtxoStakeDescriptionsPerCoinstakeWorker - 1) / UtxoStakeDescriptionsPerCoinstakeWorker;
            var workers = new Task[workerCount];
            var workerContexts = new CoinstakeWorkerContext[workerCount];

            var workersResult = new CoinstakeWorkerResult();
            for (int workerIndex = 0; workerIndex < workerCount; workerIndex++)
            {
                var cwc = new CoinstakeWorkerContext
                {
                    Index = workerIndex,
                    Logger = this.loggerFactory.CreateLogger(this.GetType().FullName, $"[Worker #{workerIndex}] "),
                    utxoStakeDescriptions = new List<UtxoStakeDescription>(),
                    CoinstakeContext = coinstakeContext,
                    Result = workersResult
                };

                int stakingUtxoCount = Math.Min(stakingUtxoDescriptions.Count - coinIndex, UtxoStakeDescriptionsPerCoinstakeWorker);
                cwc.utxoStakeDescriptions.AddRange(stakingUtxoDescriptions.GetRange(coinIndex, stakingUtxoCount));
                coinIndex += stakingUtxoCount;
                workerContexts[workerIndex] = cwc;

                workers[workerIndex] = Task.Run(() => this.CoinstakeWorker(cwc, chainTip, block, minimalAllowedTime, searchInterval));
            }

            await Task.WhenAll(workers).ConfigureAwait(false);

            if (workersResult.KernelFoundIndex == CoinstakeWorkerResult.KernelNotFound)
            {
                this.logger.LogTrace("(-)[KERNEL_NOT_FOUND]:false");
                return false;
            }

            this.logger.LogTrace("Worker #{0} found the kernel.", workersResult.KernelFoundIndex);

            // Get reward for newly created block.
            long reward = fees + this.consensusManager.ConsensusRules.GetRule<PosCoinviewRule>().GetProofOfStakeReward(chainTip.Height + 1);
            if (reward <= 0)
            {
                // TODO: This can't happen unless we remove reward for mined block.
                // If this can happen over time then this check could be done much sooner
                // to avoid a lot of computation.
                this.logger.LogTrace("(-)[NO_REWARD]:false");
                return false;
            }

            // Input to coinstake transaction.
            UtxoStakeDescription coinstakeInput = workersResult.KernelCoin;

            // Total amount of input values in coinstake transaction.
            long coinstakeOutputValue = coinstakeInput.TxOut.Value + reward;

            int eventuallyStakableUtxosCount = utxoStakeDescriptions.Count;
            Transaction coinstakeTx = this.PrepareCoinStakeTransactions(chainTip.Height, coinstakeContext, coinstakeOutputValue, eventuallyStakableUtxosCount, ourWeight);

            // Sign.
            if (!this.SignTransactionInput(coinstakeInput, coinstakeTx))
            {
                this.logger.LogTrace("(-)[SIGN_FAILED]:false");
                return false;
            }

            // Limit size.
            int serializedSize = coinstakeContext.CoinstakeTx.GetSerializedSize(ProtocolVersion.ALT_PROTOCOL_VERSION, SerializationType.Network);
            if (serializedSize >= (MaxBlockSizeGen / 5))
            {
                this.logger.LogTrace("Coinstake size {0} bytes exceeded limit {1} bytes.", serializedSize, MaxBlockSizeGen / 5);
                this.logger.LogTrace("(-)[SIZE_EXCEEDED]:false");
                return false;
            }

            // Successfully generated coinstake.
            return true;
        }

        internal virtual Transaction PrepareCoinStakeTransactions(int currentChainHeight, CoinstakeContext coinstakeContext, long coinstakeOutputValue, int utxosCount, long amountStaked)
        {
            // Split stake into SplitFactor utxos if above threshold.
            bool shouldSplitStake = this.ShouldSplitStake(utxosCount, amountStaked, coinstakeOutputValue, currentChainHeight);

            int lastOutputIndex = coinstakeContext.CoinstakeTx.Outputs.Count - 1;

            if (!shouldSplitStake)
            {
                coinstakeContext.CoinstakeTx.Outputs[lastOutputIndex].Value = coinstakeOutputValue;
                this.logger.LogTrace("Coinstake output value is {0}.", coinstakeContext.CoinstakeTx.Outputs[lastOutputIndex].Value);
                this.logger.LogTrace("(-)[NO_SPLIT]:{0}", coinstakeContext.CoinstakeTx);
                return coinstakeContext.CoinstakeTx;
            }

            long splitValue = coinstakeOutputValue / SplitFactor;
            long remainder = coinstakeOutputValue - (SplitFactor - 1) * splitValue;
            coinstakeContext.CoinstakeTx.Outputs[lastOutputIndex].Value = remainder;

            for (int i = 0; i < SplitFactor - 1; i++)
            {
                var split = new TxOut(splitValue, coinstakeContext.CoinstakeTx.Outputs[lastOutputIndex].ScriptPubKey);
                coinstakeContext.CoinstakeTx.Outputs.Add(split);
            }

            this.logger.LogTrace("Coinstake output value has been split into {0} outputs of {1} and a remainder of {2}.", SplitFactor - 1, splitValue, remainder);

            return coinstakeContext.CoinstakeTx;
        }

        /// <summary>
        /// Worker method that tries to find coinstake kernel within a small list of UTXOs.
        /// <para>
        /// There are multiple worker tasks created, each checking subset of all available UTXOs.
        /// This allows the kernel finding task to be processed on multiple processors in parallel.
        /// </para>
        /// </summary>
        /// <param name="context">Context information with worker task description. Results of the worker's attempt are also stored in this context.</param>
        /// <param name="chainTip">Tip of the best chain. Used only to stop working as soon as the chain advances.</param>
        /// <param name="block">Template of the block that we are trying to mine.</param>
        /// <param name="minimalAllowedTime">Minimal valid timestamp for new coinstake transaction.</param>
        /// <param name="searchInterval">Length of an unexplored block time space in seconds. It only makes sense to look for a solution within this interval.</param>
        protected virtual void CoinstakeWorker(CoinstakeWorkerContext context, ChainedHeader chainTip, Block block, long minimalAllowedTime, long searchInterval)
        {
            context.Logger.LogTrace("Going to process {0} UTXOs.", context.utxoStakeDescriptions.Count);

            // Sort staking UTXOs by amount, so that highest amounts are tried first
            // because they have greater chance to succeed and thus saving some work.
            List<UtxoStakeDescription> orderedUtxoStakeDescriptions = context.utxoStakeDescriptions.OrderByDescending(o => o.TxOut.Value).ToList();

            bool stopWork = false;
            foreach (UtxoStakeDescription utxoStakeInfo in orderedUtxoStakeDescriptions)
            {
                context.Logger.LogTrace("Trying UTXO from address '{0}', output amount {1}.", utxoStakeInfo.Address.Address, utxoStakeInfo.TxOut.Value);

                // Script of the first coinstake input.
                Script scriptPubKeyKernel = utxoStakeInfo.TxOut.ScriptPubKey;
                if (!this.ValidStakingTemplates.Any(a => a.Value.CheckScriptPubKey(scriptPubKeyKernel)))
                {
                    context.Logger.LogTrace("Kernel type must be {0}, kernel rejected.", string.Join(" or ", this.ValidStakingTemplates.Keys));
                    continue;
                }

                for (uint n = 0; n < searchInterval; n++)
                {
                    if (context.Result.KernelFoundIndex != CoinstakeWorkerResult.KernelNotFound)
                    {
                        context.Logger.LogTrace("Different worker #{0} already found kernel, stopping work.", context.Result.KernelFoundIndex);
                        stopWork = true;
                        break;
                    }

                    if (this.stakeCancellationTokenSource.Token.IsCancellationRequested)
                    {
                        context.Logger.LogTrace("Application shutdown detected, stopping work.");
                        stopWork = true;
                        break;
                    }

                    if (chainTip != this.chain.Tip)
                    {
                        context.Logger.LogTrace("Chain advanced, stopping work.");
                        stopWork = true;
                        break;
                    }

                    uint txTime = context.CoinstakeContext.CoinstakeTx.Time - n;

                    // Once we reach previous block time + 1, we can't go any lower
                    // because it is required that the block time is greater than the previous block time.
                    if (txTime < minimalAllowedTime)
                        break;

                    if ((txTime & PosConsensusOptions.StakeTimestampMask) != 0)
                        continue;

                    context.Logger.LogTrace("Trying with transaction time {0}.", txTime);

                    try
                    {
                        var prevoutStake = new OutPoint(utxoStakeInfo.UtxoSet.TransactionId, utxoStakeInfo.OutPoint.N);

                        var contextInformation = new PosRuleContext(BlockStake.Load(block));

                        this.stakeValidator.CheckKernel(contextInformation, chainTip, block.Header.Bits, txTime, prevoutStake);

                        if (context.Result.SetKernelFoundIndex(context.Index))
                        {
                            context.Logger.LogTrace("Kernel found with solution hash '{0}'.", contextInformation.HashProofOfStake);

                            Wallet.Wallet wallet = this.walletManager.GetWalletByName(utxoStakeInfo.Secret.WalletName);
                            context.CoinstakeContext.Key = wallet.GetExtendedPrivateKeyForAddress(utxoStakeInfo.Secret.WalletPassword, utxoStakeInfo.Address).PrivateKey;
                            utxoStakeInfo.Key = context.CoinstakeContext.Key;

                            context.CoinstakeContext.CoinstakeTx.Time = txTime;
                            context.CoinstakeContext.CoinstakeTx.AddInput(new TxIn(prevoutStake));
                            Script scriptPubKeyOut;

                            // Create a pubkey script form the current script.
                            string scriptType = this.ValidStakingTemplates.Single(t => t.Value.CheckScriptPubKey(utxoStakeInfo.TxOut.ScriptPubKey)).Key;

                            // Default behavior.
                            if ((scriptType == "P2PK") || (scriptType == "P2PKH"))
                            {
                                scriptPubKeyOut = PayToPubkeyTemplate.Instance.GenerateScriptPubKey(context.CoinstakeContext.Key.PubKey);
                            }
                            else
                            // Support for otherwise unsupported script types.
                            {
                                context.CoinstakeContext.CoinstakeTx.Outputs.Add(new TxOut(Money.Zero,
                                    new Script(OpcodeType.OP_RETURN, Op.GetPushOp(utxoStakeInfo.Key.PubKey.Compress().ToBytes()))));

                                scriptPubKeyOut = utxoStakeInfo.TxOut.ScriptPubKey;
                            }

                            context.CoinstakeContext.CoinstakeTx.Outputs.Add(new TxOut(0, scriptPubKeyOut));
                            context.Result.KernelCoin = utxoStakeInfo;

                            context.Logger.LogTrace("Kernel accepted, coinstake input is '{0}', stopping work.", prevoutStake);
                        }
                        else context.Logger.LogTrace("Kernel found, but worker #{0} announced its kernel earlier, stopping work.", context.Result.KernelFoundIndex);

                        stopWork = true;
                    }
                    catch (ConsensusErrorException cex)
                    {
                        context.Logger.LogTrace("Checking kernel failed with exception: {0}.", cex.Message);
                        if (cex.ConsensusError == ConsensusErrors.StakeHashInvalidTarget)
                            continue;

                        stopWork = true;
                    }

                    if (stopWork) break;
                }

                // If kernel is found or error occurred, stop searching.
                if (stopWork) break;
            }
        }

        /// <summary>
        /// Signs input of a transaction.
        /// </summary>
        /// <param name="input">Transaction input.</param>
        /// <param name="transaction">Transaction being built.</param>
        /// <returns><c>true</c> if the function succeeds, <c>false</c> otherwise.</returns>
        protected virtual bool SignTransactionInput(UtxoStakeDescription input, Transaction transaction)
        {
            bool res = false;
            try
            {
                var transactionBuilder = new TransactionBuilder(this.network)
                    .AddKeys(input.Key)
                    .AddCoins(new Coin(input.OutPoint, input.TxOut));

                foreach (BuilderExtension extension in this.walletManager.GetTransactionBuilderExtensionsForStaking())
                    transactionBuilder.Extensions.Add(extension);

                transactionBuilder.SignTransactionInPlace(transaction);

                res = true;
            }
            catch (Exception e)
            {
                this.logger.LogDebug("Exception occurred: {0}", e.ToString());
            }

            return res;
        }

        /// <inheritdoc/>
        public async Task<Money> GetMatureBalanceAsync(List<UtxoStakeDescription> utxoStakeDescriptions)
        {
            var money = new Money(0);
            foreach (UtxoStakeDescription utxoStakeDescription in utxoStakeDescriptions)
            {
                // Must wait until coinbase is safely deep enough in the chain before valuing it.
                if ((utxoStakeDescription.UtxoSet.IsCoinbase || utxoStakeDescription.UtxoSet.IsCoinstake) && (await this.GetBlocksCountToMaturityAsync(utxoStakeDescription).ConfigureAwait(false) > 0))
                    continue;

                money += utxoStakeDescription.TxOut.Value;
            }

            return money;
        }

        /// <summary>
        /// Selects UTXOs that are suitable for staking.
        /// <para>
        /// Such a UTXO has to be confirmed with enough confirmations - i.e. has suitable depth,
        /// and it also has to be mature and meet requirement for minimal value.
        /// </para>
        /// </summary>
        /// <param name="utxoStakeDescriptions">List of UTXO descriptions that are candidates for being used for staking.</param>
        /// <param name="chainTip">Tip of the best chain.</param>
        /// <param name="spendTime">Timestamp of the coinstake transaction.</param>
        /// <param name="targetValue">Target money amount of UTXOs that can be used for staking.</param>
        /// <returns>List of UTXO descriptions that meet the requirements for staking.</returns>
        internal async Task<List<UtxoStakeDescription>> GetUtxoStakeDescriptionsSuitableForStakingAsync(List<UtxoStakeDescription> utxoStakeDescriptions, ChainedHeader chainTip, uint spendTime, long targetValue)
        {
            var res = new List<UtxoStakeDescription>();

            long currentValue = 0;
            long requiredDepth = ((PosConsensusOptions)this.network.Consensus.Options).GetStakeMinConfirmations(chainTip.Height + 1, this.network) - 1;
            foreach (UtxoStakeDescription utxoStakeDescription in utxoStakeDescriptions.OrderByDescending(x => x.TxOut.Value))
            {
                int depth = await this.GetDepthInMainChainAsync(utxoStakeDescription).ConfigureAwait(false);
                this.logger.LogTrace("Checking if UTXO '{0}' value {1} can be added, its depth is {2}.", utxoStakeDescription.OutPoint, utxoStakeDescription.TxOut.Value, depth);

                if (depth < 1)
                {
                    this.logger.LogTrace("UTXO '{0}' is new or reorg happened.", utxoStakeDescription.OutPoint);
                    continue;
                }

                if (depth < requiredDepth)
                {
                    this.logger.LogTrace("UTXO '{0}' depth {1} is lower than required minimum depth {2}.", utxoStakeDescription.OutPoint, depth, requiredDepth);
                    continue;
                }

                if (utxoStakeDescription.UtxoSet.Time > spendTime)
                {
                    this.logger.LogTrace("UTXO '{0}' can't be added because its time {1} is greater than coinstake time {2}.", utxoStakeDescription.OutPoint, utxoStakeDescription.UtxoSet.Time, spendTime);
                    continue;
                }

                int toMaturity = await this.GetBlocksCountToMaturityAsync(utxoStakeDescription).ConfigureAwait(false);
                if (toMaturity > 0)
                {
                    this.logger.LogTrace("UTXO '{0}' can't be added because it is not mature, {1} blocks to maturity left.", utxoStakeDescription.OutPoint, toMaturity);
                    continue;
                }

                currentValue += utxoStakeDescription.TxOut.Value;

                this.logger.LogTrace("UTXO '{0}' accepted.", utxoStakeDescription.OutPoint);
                res.Add(utxoStakeDescription);

                if (currentValue >= targetValue)
                    break;
            }

            return res;
        }

        /// <summary>
        /// Calculates blocks count till UTXO is considered mature for staking.
        /// </summary>
        /// <param name="utxoStakeDescription">The UTXO stake description.</param>
        /// <returns>How many blocks are left till UTXO is considered mature for staking.</returns>
        private async Task<int> GetBlocksCountToMaturityAsync(UtxoStakeDescription utxoStakeDescription)
        {
            if (!(utxoStakeDescription.UtxoSet.IsCoinbase || utxoStakeDescription.UtxoSet.IsCoinstake))
                return 0;

            return Math.Max(0, (int)this.network.Consensus.CoinbaseMaturity + 1 - await this.GetDepthInMainChainAsync(utxoStakeDescription).ConfigureAwait(false));
        }

        /// <summary>
        /// Gets depth of transaction in blockchain.
        /// </summary>
        /// <param name="utxoStakeDescription">The UTXO stake description.</param>
        /// <returns>
        /// <c>-1</c> if not in blockchain, and not in memory pool (conflicted transaction).
        /// <c>0</c> if in memory pool, waiting to be included in a block.
        /// Value greater than <c>1</c> if included in a block. Shows how many blocks deep in the main chain.
        /// </returns>
        private async Task<int> GetDepthInMainChainAsync(UtxoStakeDescription utxoStakeDescription)
        {
            ChainedHeader chainedBlock = this.chain.GetBlock(utxoStakeDescription.HashBlock);

            if (chainedBlock == null)
                return await this.mempoolLock.ReadAsync(() => this.mempool.Exists(utxoStakeDescription.UtxoSet.TransactionId) ? 0 : -1).ConfigureAwait(false);

            return this.chain.Tip.Height - chainedBlock.Height + 1;
        }

        /// <inheritdoc/>
        public double GetDifficulty(ChainedHeader block)
        {
            double res = 1.0;

            if (block == null)
            {
                // Use consensus loop's tip rather than concurrent chain's tip
                // because consensus loop's tip is guaranteed to have block stake in the database.
                ChainedHeader tip = this.consensusManager.Tip;
                if (tip == null)
                {
                    this.logger.LogTrace("(-)[DEFAULT]:{0}", res);
                    return res;
                }

                block = this.stakeValidator.GetLastPowPosChainedBlock(this.stakeChain, tip, false);
            }

            // calculate the current shift value.
            uint shift = (block.Header.Bits >> 24) & 0xFF;
            double diff = (double)0x0000FFFF / (double)(block.Header.Bits & 0x00FFFFFF);

            // shift the difficulty up.
            while (shift < 29)
            {
                diff *= 256.0;
                shift++;
            }

            // shift the difficulty down.
            while (shift > 29)
            {
                diff /= 256.0;
                shift--;
            }

            res = diff;
            return res;
        }

        /// <inheritdoc/>
        public double GetNetworkWeight()
        {
            int interval = 72;
            double stakeKernelsAvg = 0.0;
            int stakesHandled = 0;
            long stakesTime = 0;

            // Use consensus loop's tip rather than concurrent chain's tip
            // because consensus loop's tip is guaranteed to have block stake in the database.
            ChainedHeader block = this.consensusManager.Tip;
            ChainedHeader prevStakeBlock = null;

            double res = 0.0;
            while ((block != null) && (stakesHandled < interval))
            {
                BlockStake blockStake = this.stakeChain.Get(block.HashBlock);
                if (blockStake != null && blockStake.IsProofOfStake())
                {
                    if (prevStakeBlock != null)
                    {
                        stakeKernelsAvg += this.GetDifficulty(prevStakeBlock) * (double)0x100000000;
                        stakesTime += (long)prevStakeBlock.Header.Time - (long)block.Header.Time;
                        stakesHandled++;
                    }

                    prevStakeBlock = block;
                }

                block = block.Previous;
            }

            if (stakesTime != 0) res = stakeKernelsAvg / stakesTime;

            res *= PosConsensusOptions.StakeTimestampMask + 1;

            return res;
        }

        /// <inheritdoc/>
        public Models.GetStakingInfoModel GetGetStakingInfoModel()
        {
            return (Models.GetStakingInfoModel)this.rpcGetStakingInfoModel.Clone();
        }

        /// <summary>
        /// Checks whether the coinstake should be split or not.
        /// </summary>
        /// <param name="stakedUtxosCount">Number of UTXOs that the wallet could stake, if coin base maturity and stake minimum confirmations were not taken into account.</param>
        /// <param name="amountStaked">Total amount currently at stake.</param>
        /// <param name="coinValue">Value of the coin we are considering to split.</param>
        /// <param name="chainHeight">Current height of the chain.</param>
        /// <returns><c>true</c> if the coinstake should be split, <c>false</c> otherwise.</returns>
        /// <remarks>
        /// We do not split a coin if the value of new coins after the split would be less than <see cref="MinimumSplitCoinValue" />. Because we split the coin to multiple outputs defined by split factor, we only consider coins with value at least <see cref="MinimumSplitCoinValue" /> * <see cref="SplitFactor" />.
        /// <para>
        /// If the above-mentioned criteria is satisfied, then we split the coin if its value is greater than an expected average value of coins that we would have if we have perfect distribution of the value among all our coins while having a specific number of coins that we aim for. The optimal number of coins we are looking for is calculated based on consensus settings of coin maturity and minimum required coin age for staking.
        /// </para>
        /// </remarks>
        /// <seealso cref="CoinstakeSplitLimitMultiplier" />
        /// <seealso cref="SplitFactor" />
        internal bool ShouldSplitStake(int stakedUtxosCount, long amountStaked, long coinValue, int chainHeight)
        {
            if (!this.CoinstakeSplitEnabled)
            {
                this.logger.LogTrace("(-)[SPLITTING_DISABLED]:{0}", false);
                return false;
            }

            long coinAgeLimit = ((PosConsensusOptions)this.network.Consensus.Options).GetStakeMinConfirmations(chainHeight + 1, this.network);
            long coinMaturityLimit = this.network.Consensus.CoinbaseMaturity;
            long requiredCoinAgeForStaking = Math.Max(coinMaturityLimit, coinAgeLimit);
            this.logger.LogTrace("Required coin age for staking is {0}.", requiredCoinAgeForStaking);

            long targetCoinDistributionSize = (requiredCoinAgeForStaking + 1) * CoinstakeSplitLimitMultiplier;

            bool coinAboveMinValue = coinValue > SplitFactor * (long)this.MinimumSplitCoinValue;
            bool coinAboveTargetAverage = coinValue > (amountStaked / targetCoinDistributionSize) + Money.COIN;

            bool shouldSplitCoin = coinAboveMinValue && coinAboveTargetAverage;

            return shouldSplitCoin;
        }
    }
}
