package transaction

import (
	"fmt"
	atomicFlag "github.com/ElrondNetwork/elrond-go-core/core/atomic"
	"math/big"
	"strconv"
	"strings"
	"sync"

	"github.com/ElrondNetwork/elrond-go-core/core/check"
	"github.com/ElrondNetwork/elrond-go-core/data/transaction"
	"github.com/ElrondNetwork/elrond-go/facade"
	"github.com/ElrondNetwork/elrond-go/process"
	"github.com/ElrondNetwork/elrond-go/process/smartContract"
	"github.com/ElrondNetwork/elrond-go/process/txsimulator"
	"github.com/ElrondNetwork/elrond-go/sharding"
	"github.com/ElrondNetwork/elrond-go/state"
	vmcommon "github.com/ElrondNetwork/elrond-vm-common"
)

const dummySignature = "01010101"
const gasRemainedSplitString = "gas remained = "
const gasUsedSlitString = "gas used = "

type transactionCostEstimator struct {
	accounts         state.AccountsAdapter
	shardCoordinator sharding.Coordinator
	txTypeHandler    process.TxTypeHandler
	feeHandler       process.FeeHandler
	txSimulator      facade.TransactionSimulatorProcessor
	mutExecution     sync.RWMutex

	flagTooMuchGasV2Msg        atomicFlag.Flag
	tooMuchGasV2MsgEnableEpoch uint32
}

// NewTransactionCostEstimator will create a new transaction cost estimator
func NewTransactionCostEstimator(
	txTypeHandler process.TxTypeHandler,
	feeHandler process.FeeHandler,
	txSimulator facade.TransactionSimulatorProcessor,
	accounts state.AccountsAdapter,
	shardCoordinator sharding.Coordinator,
	epochNotifier process.EpochNotifier,
	tooMuchGasMessageV2EnableEpoch uint32,
) (*transactionCostEstimator, error) {
	if check.IfNil(txTypeHandler) {
		return nil, process.ErrNilTxTypeHandler
	}
	if check.IfNil(feeHandler) {
		return nil, process.ErrNilEconomicsFeeHandler
	}
	if check.IfNil(txSimulator) {
		return nil, txsimulator.ErrNilTxSimulatorProcessor
	}
	if check.IfNil(shardCoordinator) {
		return nil, process.ErrNilShardCoordinator
	}
	if check.IfNil(accounts) {
		return nil, process.ErrNilAccountsAdapter
	}
	if check.IfNil(epochNotifier) {
		return nil, process.ErrNilEpochNotifier
	}

	tce := &transactionCostEstimator{
		txTypeHandler:              txTypeHandler,
		feeHandler:                 feeHandler,
		txSimulator:                txSimulator,
		accounts:                   accounts,
		shardCoordinator:           shardCoordinator,
		tooMuchGasV2MsgEnableEpoch: tooMuchGasMessageV2EnableEpoch,
		flagTooMuchGasV2Msg:        atomicFlag.Flag{},
	}

	epochNotifier.RegisterNotifyHandler(tce)

	return tce, nil
}

// ComputeTransactionGasLimit will calculate how many gas units a transaction will consume
func (tce *transactionCostEstimator) ComputeTransactionGasLimit(tx *transaction.Transaction) (*transaction.CostResponse, error) {
	tce.mutExecution.RLock()
	defer tce.mutExecution.RUnlock()

	txTypeOnSender, txTypeOnDestination := tce.txTypeHandler.ComputeTransactionType(tx)
	if txTypeOnSender == process.MoveBalance && txTypeOnDestination == process.MoveBalance {
		return tce.computeMoveBalanceCost(tx), nil
	}

	switch txTypeOnSender {
	case process.SCDeployment, process.SCInvoking, process.BuiltInFunctionCall, process.MoveBalance:
		return tce.simulateTransactionCost(tx, txTypeOnSender)
	case process.RelayedTx, process.RelayedTxV2:
		// TODO implement in the next PR
		return &transaction.CostResponse{
			GasUnits:      0,
			ReturnMessage: "cannot compute cost of the relayed transaction",
		}, nil
	default:
		return &transaction.CostResponse{
			GasUnits:      0,
			ReturnMessage: process.ErrWrongTransaction.Error(),
		}, nil
	}
}

func (tce *transactionCostEstimator) computeMoveBalanceCost(tx *transaction.Transaction) *transaction.CostResponse {
	gasUnits := tce.feeHandler.ComputeGasLimit(tx)

	return &transaction.CostResponse{
		GasUnits:      gasUnits,
		ReturnMessage: "",
	}
}

func (tce *transactionCostEstimator) simulateTransactionCost(tx *transaction.Transaction, txType process.TransactionType) (*transaction.CostResponse, error) {
	err := tce.addMissingFieldsIfNeeded(tx)
	if err != nil {
		return nil, err
	}

	res, err := tce.txSimulator.ProcessTx(tx)
	if err != nil {
		return &transaction.CostResponse{
			GasUnits:      0,
			ReturnMessage: err.Error(),
		}, nil
	}

	isMoveBalanceOk := txType == process.MoveBalance && res.FailReason == ""
	if isMoveBalanceOk {
		return &transaction.CostResponse{
			GasUnits:      tce.feeHandler.ComputeGasLimit(tx),
			ReturnMessage: "",
		}, nil

	}

	if res.FailReason != "" {
		return &transaction.CostResponse{
			GasUnits:      0,
			ReturnMessage: res.FailReason,
		}, nil
	}

	if res.VMOutput == nil {
		return &transaction.CostResponse{
			GasUnits:             0,
			ReturnMessage:        process.ErrNilVMOutput.Error(),
			SmartContractResults: nil,
		}, nil
	}

	if res.VMOutput.ReturnCode == vmcommon.Ok {
		return &transaction.CostResponse{
			GasUnits:             tce.computeGasUnitsBasedOnVMOutput(tx, res.VMOutput),
			ReturnMessage:        "",
			SmartContractResults: res.ScResults,
		}, nil
	}

	return &transaction.CostResponse{
		GasUnits:             0,
		ReturnMessage:        fmt.Sprintf("%s %s", res.VMOutput.ReturnCode.String(), res.VMOutput.ReturnMessage),
		SmartContractResults: res.ScResults,
	}, nil
}

func (tce *transactionCostEstimator) computeGasUnitsBasedOnVMOutput(tx *transaction.Transaction, vmOutput *vmcommon.VMOutput) uint64 {
	isTooMuchGasProvided := strings.Contains(vmOutput.ReturnMessage, smartContract.TooMuchGasProvidedMessage)
	if !isTooMuchGasProvided {
		return tx.GasLimit - vmOutput.GasRemaining
	}

	if tce.flagTooMuchGasV2Msg.IsSet() {
		gasNeededForProcessing := extractGasRemainedFromMessage(vmOutput.ReturnMessage, gasUsedSlitString)
		return tce.feeHandler.ComputeGasLimit(tx) + gasNeededForProcessing
	}

	return tx.GasLimit - extractGasRemainedFromMessage(vmOutput.ReturnMessage, gasRemainedSplitString)
}

func extractGasRemainedFromMessage(message string, splitString string) uint64 {
	splitMessage := strings.Split(message, splitString)
	if len(splitMessage) < 2 {
		log.Warn("extractGasRemainedFromMessage", "error", "cannot split message", "message", message)
		return 0
	}

	gasValue, err := strconv.ParseUint(splitMessage[1], 10, 64)
	if err != nil {
		log.Warn("extractGasRemainedFromMessage", "error", err.Error())
		return 0
	}

	return gasValue
}

func (tce *transactionCostEstimator) addMissingFieldsIfNeeded(tx *transaction.Transaction) error {
	if tx.GasPrice == 0 {
		tx.GasPrice = tce.feeHandler.MinGasPrice()
	}
	if len(tx.Signature) == 0 {
		tx.Signature = []byte(dummySignature)
	}
	if tx.GasLimit == 0 {
		var err error
		tx.GasLimit, err = tce.getTxGasLimit(tx)

		return err
	}

	return nil
}

func (tce *transactionCostEstimator) getTxGasLimit(tx *transaction.Transaction) (uint64, error) {
	selfShardID := tce.shardCoordinator.SelfId()
	maxGasLimitPerBlock := tce.feeHandler.MaxGasLimitPerBlock(selfShardID) - 1

	senderShardID := tce.shardCoordinator.ComputeId(tx.SndAddr)
	if tce.shardCoordinator.SelfId() != senderShardID {
		return maxGasLimitPerBlock, nil
	}

	accountHandler, err := tce.accounts.LoadAccount(tx.SndAddr)
	if err != nil {
		return 0, err
	}

	if check.IfNil(accountHandler) {
		return 0, process.ErrInsufficientFee
	}

	accountSender, ok := accountHandler.(state.UserAccountHandler)
	if !ok {
		return 0, process.ErrInsufficientFee
	}

	accountSenderBalance := accountSender.GetBalance()
	tx.GasLimit = maxGasLimitPerBlock
	txFee := tce.feeHandler.ComputeTxFee(tx)
	if txFee.Cmp(accountSenderBalance) > 0 && big.NewInt(0).Cmp(accountSenderBalance) != 0 {
		return tce.feeHandler.ComputeGasLimitBasedOnBalance(tx, accountSenderBalance)
	}

	return maxGasLimitPerBlock, nil
}

// EpochConfirmed  is called whenever a new epoch is confirmed
func (tce *transactionCostEstimator) EpochConfirmed(epoch uint32, timestamp uint64) {
	tce.flagTooMuchGasV2Msg.SetValue(epoch >= tce.tooMuchGasV2MsgEnableEpoch)
	log.Debug("transactionCostEstimator: too much gas V2 message", "enabled", tce.flagTooMuchGasV2Msg.IsSet())
}

// IsInterfaceNil returns true if there is no value under the interface
func (tce *transactionCostEstimator) IsInterfaceNil() bool {
	return tce == nil
}
