package chain

import (
	"fmt"
	"reflect"
	"time"

	"github.com/aergoio/aergo-actor/actor"
	"github.com/aergoio/aergo/v2/internal/enc/base58"
	"github.com/aergoio/aergo/v2/pkg/component"
	"github.com/aergoio/aergo/v2/types"
	"github.com/aergoio/aergo/v2/types/message"
)

type ChainVerifier struct {
	*SubComponent
	cs            *ChainService
	IChainHandler //to use chain APIs
	*Core
	reader        *BlockReader
	err           error
	targetBlockNo uint64
	stage         TestStage
}

func newChainVerifier(cs *ChainService, core *Core, targetBlockNo uint64) *ChainVerifier {
	chainVerifier := &ChainVerifier{IChainHandler: cs, Core: core, cs: cs, targetBlockNo: targetBlockNo}
	chainVerifier.SubComponent = NewSubComponent(chainVerifier, cs.BaseComponent, chainVerifierName, 1)

	var (
		bestBlock *types.Block
		err       error
	)

	if bestBlock, err = core.cdb.GetBestBlock(); err != nil {
		logger.Fatal().Msg("can't get best block in newChainVerifier()")
	}

	chainVerifier.reader = &BlockReader{cdb: core.cdb, curNo: 0, bestNo: bestBlock.BlockNo()}

	return chainVerifier
}

func (cv *ChainVerifier) Receive(context actor.Context) {
	defer RecoverExit()

	switch msg := context.Message().(type) {
	case *message.VerifyStart:

	case *actor.Started:
		logger.Info().Msg("verify chain service start")

		for !cv.cs.isRecovered() {
			logger.Debug().Msg("recovery of chain doesn't finished")
			time.Sleep(time.Second * 5)
		}

		if err := cv.VerifyChain(); err != nil {
			logger.Error().Err(err).Msg("failed to verify chain")
			cv.err = err
		}

		logger.Info().Msg("verify chain finished")

	case *actor.Stopping, *actor.Stopped, *component.CompStatReq: // do nothing
	default:
		debug := fmt.Sprintf("[%s] Missed message. (%v) %s", cv.name, reflect.TypeOf(msg), msg)
		logger.Debug().Msg(debug)
	}
}

type BlockReader struct {
	cdb *ChainDB

	genesisBlock *types.Block
	curNo        uint64
	bestNo       uint64
}

func (br *BlockReader) getNext() (*types.Block, error) {
	if br.genesisBlock == nil {
		br.curNo = 0
	} else {
		if br.curNo+1 > br.bestNo {
			return nil, nil
		}
		br.curNo++
	}

	block, err := br.cdb.GetBlockByNo(br.curNo)
	if err != nil {
		logger.Error().Err(err).Uint64("no", br.curNo).Msg("failed to get next block")
		return nil, err
	}

	if br.curNo == 0 {
		br.genesisBlock = block
	}

	return block, nil
}

type TestStage int

const (
	TestPrevBlock TestStage = 0 + iota
	TestCurBlock
	TestBlockExecute
	TestComplete
)

var TestStageStr = []string{
	"Test if previous block exist",
	"Test if target block exist",
	"Test if block execution succeed",
	"All tests completed",
}

func (cv *ChainVerifier) VerifyBlockWithReport() error {
	var (
		err       error
		block     *types.Block
		prevBlock *types.Block
	)

	logger.Info().Msg("================== [VERIFYBLOCK] start =================")
	logger.Info().Msgf("Target block no : %d", cv.targetBlockNo)

	// set sdb root with previous block

	if cv.targetBlockNo <= 0 {
		logger.Error().Msg("target block number must be greater than 0")
	}

	cv.setStage(TestPrevBlock)
	if prevBlock, err = cv.cdb.GetBlockByNo(cv.targetBlockNo - 1); err != nil {
		goto END
	}
	cv.report(prevBlock, nil, nil)

	cv.Core.sdb.SetRoot(prevBlock.GetHeader().GetBlocksRootHash())

	cv.setStage(TestCurBlock)
	if block, err = cv.cdb.GetBlockByNo(cv.targetBlockNo); err != nil {
		goto END
	}
	cv.report(prevBlock, block, err)

	cv.setStage(TestBlockExecute)
	if err = cv.verifyBlock(block); err != nil {
		goto END
	}
	cv.report(prevBlock, block, nil)

	cv.setStage(TestComplete)
END:
	cv.report(prevBlock, block, err)

	logger.Info().Msg("================== [VERIFYBLOCK] end =================")

	return err
}

func (cv *ChainVerifier) setStage(stage TestStage) {
	cv.stage = stage
}

func (cv *ChainVerifier) report(prevBlock *types.Block, targetBlock *types.Block, err error) {
	var (
		report    string
		stageName string
	)

	if cv.stage == TestComplete {
		logger.Info().Msg(TestStageStr[TestComplete])
		return
	}

	stageName = TestStageStr[cv.stage]
	if err != nil {
		report += fmt.Sprintf("%s: FAILED.", stageName)
		report += fmt.Sprintf(" [Error] = %s", err.Error())
		logger.Info().Msg(report)
		return
	}

	report += fmt.Sprintf("%s: PASS. ", stageName)

	switch cv.stage {
	case TestPrevBlock:
		report += fmt.Sprintf("[description] prev block hash=%s, prev stage root=%s", prevBlock.ID(),
			base58.Encode(prevBlock.GetHeader().GetBlocksRootHash()))

	case TestCurBlock:
		report += fmt.Sprintf("[description] target block hash=%s", targetBlock.ID())

	case TestBlockExecute:
		report += fmt.Sprintf("[description] tx Merkle = %s", base58.Encode(targetBlock.GetHeader().GetTxsRootHash()))
		report += fmt.Sprintf(", state Root = %s", base58.Encode(targetBlock.GetHeader().GetBlocksRootHash()))
		report += fmt.Sprintf(", all transaction passed")
	}

	logger.Info().Msg(report)
}

func (cv *ChainVerifier) VerifyChain() error {
	var (
		err   error
		block *types.Block
	)

	if cv.targetBlockNo > 0 {
		return cv.VerifyBlockWithReport()
	}

	logger.Info().Msg("start verifyChain")

	// get genesis block
	if block, err = cv.reader.getNext(); err != nil || block == nil {
		logger.Error().Err(err).Msg("failed to get genesis block")
		return err
	}

	if err = cv.Core.sdb.SetRoot(block.GetHeader().GetBlocksRootHash()); err != nil {
		logger.Error().Err(err).Msg("failed to set root of sdb to root hash of genesis block")
		return err
	}

	for {
		if block, err = cv.reader.getNext(); err != nil || block == nil {
			return err
		}

		if err := cv.verifyBlock(block); err != nil {
			logger.Error().Err(err).Uint64("no", block.BlockNo()).Str("block", block.ID()).Msg("failed to verify block")
			return err
		}
	}
}

func (cv *ChainVerifier) IsRunning() bool {
	return cv.reader.bestNo > cv.reader.curNo
}

func (cv *ChainVerifier) Statistics() *map[string]interface{} {
	var (
		state  string
		errStr string
	)

	if cv.err != nil {
		errStr = cv.err.Error()
	}

	if cv.IsRunning() {
		state = "running"
	} else {
		state = "finished"
	}

	return &map[string]interface{}{
		"verify":       state,
		"verifyBestNo": cv.reader.bestNo,
		"verifyCurNo":  cv.reader.curNo,
		"verifyErr":    errStr,
	}
}
