package chain

import (
	"bytes"
	"errors"
	"math/big"
	"time"

	"github.com/aergoio/aergo/v2/types"
	"github.com/aergoio/aergo/v2/types/message"
)

// StubSyncer receive Syncer, P2P, Chain Service actor message
type StubBlockChain struct {
	Best   int
	Hashes []([]byte)
	Blocks []*types.Block

	BestBlock *types.Block
}

var _ types.ChainAccessor = (*StubBlockChain)(nil)

var (
	ErrNotExistHash  = errors.New("not exist hash")
	ErrNotExistBlock = errors.New("not exist block of the hash")

	testBV = types.DummyBlockVersionner(0)
)

func NewStubBlockChain(size int) *StubBlockChain {
	if size < 10000 {
		size = 10000
	}

	tchain := &StubBlockChain{Best: -1}

	tchain.Hashes = make([][]byte, size+1)
	tchain.Blocks = make([]*types.Block, size+1)

	return tchain
}

func (tchain *StubBlockChain) GenAddBlock() {
	var bi *types.BlockHeaderInfo
	var prevBlockRootHash []byte
	if tchain.BestBlock != nil {
		bi = types.NewBlockHeaderInfoFromPrevBlock(tchain.BestBlock, time.Now().UnixNano(), types.DummyBlockVersionner(0))
		prevBlockRootHash = tchain.BestBlock.GetHeader().BlocksRootHash
	} else {
		cid, _ := types.NewChainID().Bytes()
		bi = &types.BlockHeaderInfo{Ts: time.Now().UnixNano(), ChainId: cid}
	}
	bi.Ts = time.Now().UnixNano()
	newBlock := types.NewBlock(bi, prevBlockRootHash, nil, nil, nil, nil)
	tchain.AddBlock(newBlock)
	time.Sleep(time.Nanosecond * 3)
}

func (tchain *StubBlockChain) AddBlock(newBlock *types.Block) error {
	if newBlock.BlockNo() != uint64(tchain.Best+1) {
		return ErrBlockOrphan
	}
	tchain.Best += 1
	tchain.Hashes[tchain.Best] = newBlock.BlockHash()
	tchain.Blocks[tchain.Best] = newBlock
	tchain.BestBlock = newBlock

	return nil
}

func (tchain *StubBlockChain) GetHashes(prevInfo *types.BlockInfo, count uint64) ([]message.BlockHash, error) {
	if tchain.Best < int(prevInfo.No+count) {
		return nil, ErrNotExistHash
	}

	start := prevInfo.No + 1
	resHashes := tchain.Hashes[start : start+count]

	blkHashes := make([]message.BlockHash, 0)
	for _, hash := range resHashes {
		blkHashes = append(blkHashes, hash)
	}

	return blkHashes, nil
}

func (tchain *StubBlockChain) GetBlockInfo(no uint64) *types.BlockInfo {
	return &types.BlockInfo{Hash: tchain.Hashes[no], No: no}
}

func (tchain *StubBlockChain) GetBlockByNo(no uint64) *types.Block {
	return tchain.Blocks[no]
}

func (tchain *StubBlockChain) GetBlocks(hashes []message.BlockHash) ([]*types.Block, error) {
	startNo := -1

	for i, block := range tchain.Blocks {
		if bytes.Equal(block.GetHash(), hashes[0]) {
			startNo = i
			break
		}
	}

	if startNo == -1 {
		return nil, ErrNotExistBlock
	}

	resultBlocks := make([]*types.Block, 0)
	i := startNo
	for _, hash := range hashes {
		if !bytes.Equal(tchain.Blocks[i].GetHash(), hash) {
			return nil, ErrNotExistBlock
		}

		resultBlocks = append(resultBlocks, tchain.Blocks[i])
		i++
	}

	return resultBlocks, nil
}

func (tchain *StubBlockChain) GetGenesisInfo() *types.Genesis {
	// Not implemented. It should be implemented later if any test is related
	// to genesis info.
	return nil
}

func (tchain *StubBlockChain) GetConsensusInfo() string {
	return ""
}

func (tchain *StubBlockChain) GetChainStats() string {
	return ""
}

func (tchain *StubBlockChain) GetSystemValue(key types.SystemValue) (*big.Int, error) {
	return nil, nil
}

func (tchain *StubBlockChain) GetEnterpriseConfig(key string) (*types.EnterpriseConfig, error) {
	return nil, nil
}

func (tchain *StubBlockChain) GetBestBlock() (*types.Block, error) {
	return tchain.BestBlock, nil
}

func (tchain *StubBlockChain) GetBlock(blockHash []byte) (*types.Block, error) {
	for _, block := range tchain.Blocks {
		if bytes.Equal(block.GetHash(), blockHash) {
			return block, nil
		}
	}

	return nil, ErrNotExistBlock
}

func (tchain *StubBlockChain) GetHashByNo(blockNo types.BlockNo) ([]byte, error) {
	if uint64(len(tchain.Hashes)) <= blockNo {
		return nil, ErrNotExistHash
	}

	return tchain.Hashes[blockNo], nil
}

// TODO refactoring with getAnchorsNew()
func (tchain *StubBlockChain) GetAnchors() (ChainAnchor, types.BlockNo, error) {
	//from top : 8 * 32 = 256
	anchors := make(ChainAnchor, 0)
	cnt := MaxAnchors
	logger.Debug().Msg("get anchors")

	bestBlock, _ := tchain.GetBestBlock()
	blkNo := bestBlock.BlockNo()
	var lastNo types.BlockNo
LOOP:
	for i := 0; i < cnt; i++ {
		blockHash, err := tchain.GetHashByNo(blkNo)
		if err != nil {
			logger.Info().Msg("assertion - hash get failed")
			// assertion!
			return nil, 0, err
		}

		anchors = append(anchors, blockHash)
		lastNo = blkNo

		logger.Debug().Uint64("no", blkNo).Msg("anchor added")

		switch {
		case blkNo == 0:
			break LOOP
		case blkNo < Skip:
			blkNo = 0
		default:
			blkNo -= Skip
		}
	}

	return anchors, lastNo, nil
}

func (tchain *StubBlockChain) GetAncestorWithHashes(hashes [][]byte) *types.BlockInfo {
	for _, hash := range hashes {
		for j, chainHash := range tchain.Hashes {
			if bytes.Equal(hash, chainHash) {
				return &types.BlockInfo{Hash: chainHash, No: uint64(j)}
			}
		}
	}

	return nil
}

func (tchain *StubBlockChain) ChainID(bno types.BlockNo) *types.ChainID {
	return nil
}

func (tchain *StubBlockChain) HardforkHeights() map[string]types.BlockNo {
	// FIXME: maybe it should return at least latest hardfork
	return nil
}

func (tchain *StubBlockChain) Rollback(ancestor *types.BlockInfo) {
	prevBest := tchain.Best
	tchain.Best = int(ancestor.No)
	tchain.BestBlock = tchain.Blocks[tchain.Best]

	logger.Debug().Int("prev", prevBest).Int("Best", tchain.Best).Msg("test local chain is rollbacked")
}

func InitStubBlockChain(prefixChain []*types.Block, genCount int) *StubBlockChain {
	newChain := NewStubBlockChain(genCount + len(prefixChain) + 1)

	//load initial Blocks
	for _, block := range prefixChain {
		newChain.AddBlock(block)
	}

	for i := 0; i < genCount; i++ {
		newChain.GenAddBlock()
	}

	return newChain
}
