package test

import (
	"fmt"
	"os"
	"time"

	"github.com/golang/protobuf/proto"
	"github.com/vapor/blockchain/txbuilder"
	"github.com/vapor/consensus"
	"github.com/vapor/database"
	dbm "github.com/vapor/database/leveldb"
	"github.com/vapor/database/storage"
	"github.com/vapor/protocol"
	"github.com/vapor/protocol/bc"
	"github.com/vapor/protocol/bc/types"
	"github.com/vapor/protocol/vm"
)

const utxoPrefix = "UT:"

type chainTestContext struct {
	Chain *protocol.Chain
	DB    dbm.DB
	Store *database.Store
}

func (ctx *chainTestContext) validateStatus(block *types.Block) error {
	// validate in mainchain
	if !ctx.Chain.InMainChain(block.Hash()) {
		return fmt.Errorf("block %d is not in mainchain", block.Height)
	}

	// validate chain status and saved block
	bestBlockHeader := ctx.Chain.BestBlockHeader()
	chainBlock, err := ctx.Chain.GetBlockByHeight(block.Height)
	if err != nil {
		return err
	}

	blockHash := block.Hash()
	if bestBlockHeader.Hash() != blockHash || chainBlock.Hash() != blockHash {
		return fmt.Errorf("chain status error")
	}

	// validate tx status
	txStatus, err := ctx.Chain.GetTransactionStatus(&blockHash)
	if err != nil {
		return err
	}

	txStatusMerkleRoot, err := types.TxStatusMerkleRoot(txStatus.VerifyStatus)
	if err != nil {
		return err
	}

	if txStatusMerkleRoot != block.TransactionStatusHash {
		return fmt.Errorf("tx status error")
	}
	return nil
}

func (ctx *chainTestContext) validateExecution(block *types.Block) error {
	for _, tx := range block.Transactions {
		for _, mainchainOutputID := range tx.MainchainOutputIDs {
			utxoEntry, _ := ctx.Store.GetUtxo(&mainchainOutputID)
			if utxoEntry == nil {
				continue
			}
			if utxoEntry.Type != storage.CrosschainUTXOType {
				return fmt.Errorf("found non-mainchain utxo entry")
			}
			if !utxoEntry.Spent {
				return fmt.Errorf("utxo entry status should be spent")
			}
		}

		for _, spentOutputID := range tx.SpentOutputIDs {
			utxoEntry, _ := ctx.Store.GetUtxo(&spentOutputID)
			if utxoEntry == nil {
				continue
			}
			if utxoEntry.Type == storage.NormalUTXOType {
				return fmt.Errorf("found normal utxo entry")
			}
			if !utxoEntry.Spent {
				return fmt.Errorf("utxo entry status should be spent")
			}
		}

		for _, outputID := range tx.ResultIds {
			utxoEntry, _ := ctx.Store.GetUtxo(outputID)
			if utxoEntry == nil && isSpent(outputID, block) {
				continue
			}
			if utxoEntry.BlockHeight != block.Height {
				return fmt.Errorf("block height error, expected: %d, have: %d", block.Height, utxoEntry.BlockHeight)
			}
			if utxoEntry.Spent {
				return fmt.Errorf("utxo entry status should not be spent")
			}
		}
	}
	return nil
}

func (ctx *chainTestContext) getUtxoEntries() map[string]*storage.UtxoEntry {
	utxoEntries := make(map[string]*storage.UtxoEntry)
	iter := ctx.DB.IteratorPrefix([]byte(utxoPrefix))
	defer iter.Release()

	for iter.Next() {
		utxoEntry := new(storage.UtxoEntry)
		if err := proto.Unmarshal(iter.Value(), utxoEntry); err != nil {
			return nil
		}
		key := string(iter.Key())
		utxoEntries[key] = utxoEntry
	}
	return utxoEntries
}

func (ctx *chainTestContext) validateRollback(utxoEntries map[string]*storage.UtxoEntry) error {
	newUtxoEntries := ctx.getUtxoEntries()
	for key := range utxoEntries {
		entry, ok := newUtxoEntries[key]
		if !ok {
			return fmt.Errorf("can't find utxo after rollback")
		}
		if entry.Spent != utxoEntries[key].Spent {
			return fmt.Errorf("utxo status dismatch after rollback")
		}
	}
	return nil
}

type chainTestConfig struct {
	RollbackTo uint64     `json:"rollback_to"`
	Blocks     []*ctBlock `json:"blocks"`
}

type ctBlock struct {
	Transactions []*ctTransaction `json:"transactions"`
	Append       uint64           `json:"append"`
	Invalid      bool             `json:"invalid"`
}

func (b *ctBlock) createBlock(ctx *chainTestContext) (*types.Block, error) {
	txs := make([]*types.Tx, 0, len(b.Transactions))
	for _, t := range b.Transactions {
		tx, err := t.createTransaction(ctx, txs)
		if err != nil {
			return nil, err
		}
		txs = append(txs, tx)
	}
	return NewBlock(ctx.Chain, txs, []byte{byte(vm.OP_TRUE)})
}

type ctTransaction struct {
	Inputs  []*ctInput `json:"inputs"`
	Outputs []uint64   `json:"outputs"`
}

type ctInput struct {
	Height      uint64 `json:"height"`
	TxIndex     uint64 `json:"tx_index"`
	OutputIndex uint64 `json:"output_index"`
}

func (input *ctInput) createTxInput(ctx *chainTestContext) (*types.TxInput, error) {
	block, err := ctx.Chain.GetBlockByHeight(input.Height)
	if err != nil {
		return nil, err
	}

	spendInput, err := CreateSpendInput(block.Transactions[input.TxIndex], input.OutputIndex)
	if err != nil {
		return nil, err
	}

	return &types.TxInput{
		AssetVersion: assetVersion,
		TypedInput:   spendInput,
	}, nil
}

// create tx input spent previous tx output in the same block
func (input *ctInput) createDependencyTxInput(txs []*types.Tx) (*types.TxInput, error) {
	// sub 1 because of coinbase tx is not included in txs
	spendInput, err := CreateSpendInput(txs[input.TxIndex-1], input.OutputIndex)
	if err != nil {
		return nil, err
	}

	return &types.TxInput{
		AssetVersion: assetVersion,
		TypedInput:   spendInput,
	}, nil
}

func (t *ctTransaction) createTransaction(ctx *chainTestContext, txs []*types.Tx) (*types.Tx, error) {
	builder := txbuilder.NewBuilder(time.Now())
	sigInst := &txbuilder.SigningInstruction{}
	currentHeight := ctx.Chain.BestBlockHeight()
	for _, input := range t.Inputs {
		var txInput *types.TxInput
		var err error
		if input.Height == currentHeight+1 {
			txInput, err = input.createDependencyTxInput(txs)
		} else {
			txInput, err = input.createTxInput(ctx)
		}
		if err != nil {
			return nil, err
		}
		err = builder.AddInput(txInput, sigInst)
		if err != nil {
			return nil, err
		}
	}

	for _, amount := range t.Outputs {
		output := types.NewIntraChainOutput(*consensus.BTMAssetID, amount, []byte{byte(vm.OP_TRUE)})
		if err := builder.AddOutput(output); err != nil {
			return nil, err
		}
	}

	tpl, _, err := builder.Build()
	if err != nil {
		return nil, err
	}

	txSerialized, err := tpl.Transaction.MarshalText()
	if err != nil {
		return nil, err
	}

	tpl.Transaction.Tx.SerializedSize = uint64(len(txSerialized))
	tpl.Transaction.TxData.SerializedSize = uint64(len(txSerialized))
	return tpl.Transaction, err
}

func (cfg *chainTestConfig) Run() error {
	db := dbm.NewDB("chain_test_db", "leveldb", "chain_test_db")
	defer os.RemoveAll("chain_test_db")
	chain, store, _, err := MockChain(db)
	if err != nil {
		return err
	}
	ctx := &chainTestContext{
		Chain: chain,
		DB:    db,
		Store: store,
	}

	var utxoEntries map[string]*storage.UtxoEntry
	var rollbackBlock *types.Block
	for _, blk := range cfg.Blocks {
		block, err := blk.createBlock(ctx)
		if err != nil {
			return err
		}
		_, err = ctx.Chain.ProcessBlock(block)
		if err != nil && blk.Invalid {
			continue
		}
		if err != nil {
			return err
		}
		if err := ctx.validateStatus(block); err != nil {
			return err
		}
		if err := ctx.validateExecution(block); err != nil {
			return err
		}
		if block.Height <= cfg.RollbackTo && cfg.RollbackTo <= block.Height+blk.Append {
			utxoEntries = ctx.getUtxoEntries()
			rollbackBlock = block
		}
		if err := AppendBlocks(ctx.Chain, blk.Append); err != nil {
			return err
		}
	}

	if rollbackBlock == nil {
		return nil
	}

	// rollback and validate
	forkedChain, err := declChain("forked_chain", ctx.Chain, rollbackBlock.Height, ctx.Chain.BestBlockHeight()+1)
	defer os.RemoveAll("forked_chain")
	if err != nil {
		return err
	}

	if err := merge(forkedChain, ctx.Chain); err != nil {
		return err
	}
	return ctx.validateRollback(utxoEntries)
}

// if the output(hash) was spent in block
func isSpent(hash *bc.Hash, block *types.Block) bool {
	for _, tx := range block.Transactions {
		for _, spendOutputID := range tx.SpentOutputIDs {
			if spendOutputID == *hash {
				return true
			}
		}
	}
	return false
}
