package lite

import (
	"time"

	crypto "github.com/tendermint/tendermint/crypto"
	"github.com/tendermint/tendermint/crypto/ed25519"
	"github.com/tendermint/tendermint/crypto/secp256k1"

	"github.com/tendermint/tendermint/types"
)

// ValKeys is a helper for testing.
//
// It lets us simulate signing with many keys, either ed25519 or secp256k1.
// The main use case is to create a set, and call GenCommit
// to get properly signed header for testing.
//
// You can set different weights of validators each time you call
// ToValidators, and can optionally extend the validator set later
// with Extend or ExtendSecp
type ValKeys []crypto.PrivKey

// GenValKeys produces an array of private keys to generate commits.
func GenValKeys(n int) ValKeys {
	res := make(ValKeys, n)
	for i := range res {
		res[i] = ed25519.GenPrivKey()
	}
	return res
}

// Change replaces the key at index i.
func (v ValKeys) Change(i int) ValKeys {
	res := make(ValKeys, len(v))
	copy(res, v)
	res[i] = ed25519.GenPrivKey()
	return res
}

// Extend adds n more keys (to remove, just take a slice).
func (v ValKeys) Extend(n int) ValKeys {
	extra := GenValKeys(n)
	return append(v, extra...)
}

// GenSecpValKeys produces an array of secp256k1 private keys to generate commits.
func GenSecpValKeys(n int) ValKeys {
	res := make(ValKeys, n)
	for i := range res {
		res[i] = secp256k1.GenPrivKey()
	}
	return res
}

// ExtendSecp adds n more secp256k1 keys (to remove, just take a slice).
func (v ValKeys) ExtendSecp(n int) ValKeys {
	extra := GenSecpValKeys(n)
	return append(v, extra...)
}

// ToValidators produces a list of validators from the set of keys
// The first key has weight `init` and it increases by `inc` every step
// so we can have all the same weight, or a simple linear distribution
// (should be enough for testing).
func (v ValKeys) ToValidators(init, inc int64) *types.ValidatorSet {
	res := make([]*types.Validator, len(v))
	for i, k := range v {
		res[i] = types.NewValidator(k.PubKey(), init+int64(i)*inc)
	}
	return types.NewValidatorSet(res)
}

// signHeader properly signs the header with all keys from first to last exclusive.
func (v ValKeys) signHeader(header *types.Header, first, last int) *types.Commit {
	votes := make([]*types.Vote, len(v))

	// we need this list to keep the ordering...
	vset := v.ToValidators(1, 0)

	// fill in the votes we want
	for i := first; i < last && i < len(v); i++ {
		vote := makeVote(header, vset, v[i])
		votes[vote.ValidatorIndex] = vote
	}

	res := &types.Commit{
		BlockID:    types.BlockID{Hash: header.Hash()},
		Precommits: votes,
	}
	return res
}

func makeVote(header *types.Header, vals *types.ValidatorSet, key crypto.PrivKey) *types.Vote {
	addr := key.PubKey().Address()
	idx, _ := vals.GetByAddress(addr)
	vote := &types.Vote{
		ValidatorAddress: addr,
		ValidatorIndex:   idx,
		Height:           header.Height,
		Round:            1,
		Timestamp:        time.Now().UTC(),
		Type:             types.VoteTypePrecommit,
		BlockID:          types.BlockID{Hash: header.Hash()},
	}
	// Sign it
	signBytes := vote.SignBytes(header.ChainID)
	// TODO Consider reworking makeVote API to return an error
	sig, err := key.Sign(signBytes)
	if err != nil {
		panic(err)
	}
	vote.Signature = sig

	return vote
}

// Silences warning that vals can also be merkle.Hashable
// nolint: interfacer
func genHeader(chainID string, height int64, txs types.Txs,
	vals *types.ValidatorSet, appHash, consHash, resHash []byte) *types.Header {

	return &types.Header{
		ChainID:  chainID,
		Height:   height,
		Time:     time.Now(),
		NumTxs:   int64(len(txs)),
		TotalTxs: int64(len(txs)),
		// LastBlockID
		// LastCommitHash
		ValidatorsHash:  vals.Hash(),
		DataHash:        txs.Hash(),
		AppHash:         appHash,
		ConsensusHash:   consHash,
		LastResultsHash: resHash,
	}
}

// GenCommit calls genHeader and signHeader and combines them into a Commit.
func (v ValKeys) GenCommit(chainID string, height int64, txs types.Txs,
	vals *types.ValidatorSet, appHash, consHash, resHash []byte, first, last int) Commit {

	header := genHeader(chainID, height, txs, vals, appHash, consHash, resHash)
	check := Commit{
		Header: header,
		Commit: v.signHeader(header, first, last),
	}
	return check
}

// GenFullCommit calls genHeader and signHeader and combines them into a Commit.
func (v ValKeys) GenFullCommit(chainID string, height int64, txs types.Txs,
	vals *types.ValidatorSet, appHash, consHash, resHash []byte, first, last int) FullCommit {

	header := genHeader(chainID, height, txs, vals, appHash, consHash, resHash)
	commit := Commit{
		Header: header,
		Commit: v.signHeader(header, first, last),
	}
	return NewFullCommit(commit, vals)
}
