package keeper

import (
	"bytes"
	"fmt"

	abci "github.com/tendermint/tendermint/abci/types"
	"github.com/tendermint/tendermint/crypto"

	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/cosmos/cosmos-sdk/x/stake/types"
)

// get a single validator
func (k Keeper) GetValidator(ctx sdk.Context, addr sdk.Address) (validator types.Validator, found bool) {
	store := ctx.KVStore(k.storeKey)
	b := store.Get(GetValidatorKey(addr))
	if b == nil {
		return validator, false
	}
	k.cdc.MustUnmarshalBinary(b, &validator)
	return validator, true
}

// get a single validator by pubkey
func (k Keeper) GetValidatorByPubKey(ctx sdk.Context, pubkey crypto.PubKey) (validator types.Validator, found bool) {
	store := ctx.KVStore(k.storeKey)
	addr := store.Get(GetValidatorByPubKeyIndexKey(pubkey))
	if addr == nil {
		return validator, false
	}
	return k.GetValidator(ctx, addr)
}

// set the main record holding validator details
func (k Keeper) SetValidator(ctx sdk.Context, validator types.Validator) {
	store := ctx.KVStore(k.storeKey)
	// set main store
	bz := k.cdc.MustMarshalBinary(validator)
	store.Set(GetValidatorKey(validator.Owner), bz)
}

// validator index
func (k Keeper) SetValidatorByPubKeyIndex(ctx sdk.Context, validator types.Validator) {
	store := ctx.KVStore(k.storeKey)
	// set pointer by pubkey
	store.Set(GetValidatorByPubKeyIndexKey(validator.PubKey), validator.Owner)
}

// validator index
func (k Keeper) SetValidatorByPowerIndex(ctx sdk.Context, validator types.Validator, pool types.Pool) {
	store := ctx.KVStore(k.storeKey)
	store.Set(GetValidatorsByPowerIndexKey(validator, pool), validator.Owner)
}

// validator index
func (k Keeper) SetValidatorBondedIndex(ctx sdk.Context, validator types.Validator) {
	store := ctx.KVStore(k.storeKey)
	store.Set(GetValidatorsBondedIndexKey(validator.Owner), validator.Owner)
}

// used in testing
func (k Keeper) validatorByPowerIndexExists(ctx sdk.Context, power []byte) bool {
	store := ctx.KVStore(k.storeKey)
	return store.Get(power) != nil
}

// Get the set of all validators with no limits, used during genesis dump
func (k Keeper) GetAllValidators(ctx sdk.Context) (validators []types.Validator) {
	store := ctx.KVStore(k.storeKey)
	iterator := sdk.KVStorePrefixIterator(store, ValidatorsKey)

	i := 0
	for ; ; i++ {
		if !iterator.Valid() {
			break
		}
		bz := iterator.Value()
		var validator types.Validator
		k.cdc.MustUnmarshalBinary(bz, &validator)
		validators = append(validators, validator)
		iterator.Next()
	}
	iterator.Close()
	return validators
}

// Get the set of all validators, retrieve a maxRetrieve number of records
func (k Keeper) GetValidators(ctx sdk.Context, maxRetrieve int16) (validators []types.Validator) {
	store := ctx.KVStore(k.storeKey)
	iterator := sdk.KVStorePrefixIterator(store, ValidatorsKey)

	validators = make([]types.Validator, maxRetrieve)
	i := 0
	for ; ; i++ {
		if !iterator.Valid() || i > int(maxRetrieve-1) {
			break
		}
		bz := iterator.Value()
		var validator types.Validator
		k.cdc.MustUnmarshalBinary(bz, &validator)
		validators[i] = validator
		iterator.Next()
	}
	iterator.Close()
	return validators[:i] // trim
}

//___________________________________________________________________________

// get the group of the bonded validators
func (k Keeper) GetValidatorsBonded(ctx sdk.Context) (validators []types.Validator) {
	store := ctx.KVStore(k.storeKey)

	// add the actual validator power sorted store
	maxValidators := k.GetParams(ctx).MaxValidators
	validators = make([]types.Validator, maxValidators)

	iterator := sdk.KVStorePrefixIterator(store, ValidatorsBondedIndexKey)
	i := 0
	for ; iterator.Valid(); iterator.Next() {

		// sanity check
		if i > int(maxValidators-1) {
			panic("maxValidators is less than the number of records in ValidatorsBonded Store, store should have been updated")
		}
		address := iterator.Value()
		validator, found := k.GetValidator(ctx, address)
		if !found {
			panic(fmt.Sprintf("validator record not found for address: %v\n", address))
		}

		validators[i] = validator
		i++
	}
	iterator.Close()
	return validators[:i] // trim
}

// get the group of bonded validators sorted by power-rank
func (k Keeper) GetValidatorsByPower(ctx sdk.Context) []types.Validator {
	store := ctx.KVStore(k.storeKey)
	maxValidators := k.GetParams(ctx).MaxValidators
	validators := make([]types.Validator, maxValidators)
	iterator := sdk.KVStoreReversePrefixIterator(store, ValidatorsByPowerIndexKey) // largest to smallest
	i := 0
	for {
		if !iterator.Valid() || i > int(maxValidators-1) {
			break
		}
		address := iterator.Value()
		validator, found := k.GetValidator(ctx, address)
		if !found {
			panic(fmt.Sprintf("validator record not found for address: %v\n", address))
		}
		if validator.Status() == sdk.Bonded {
			validators[i] = validator
			i++
		}
		iterator.Next()
	}
	iterator.Close()
	return validators[:i] // trim
}

//_________________________________________________________________________
// Accumulated updates to the active/bonded validator set for tendermint

// get the most recently updated validators
func (k Keeper) GetTendermintUpdates(ctx sdk.Context) (updates []abci.Validator) {
	store := ctx.KVStore(k.storeKey)

	iterator := sdk.KVStorePrefixIterator(store, TendermintUpdatesKey) //smallest to largest
	for ; iterator.Valid(); iterator.Next() {
		valBytes := iterator.Value()
		var val abci.Validator
		k.cdc.MustUnmarshalBinary(valBytes, &val)
		updates = append(updates, val)
	}
	iterator.Close()
	return
}

// remove all validator update entries after applied to Tendermint
func (k Keeper) ClearTendermintUpdates(ctx sdk.Context) {
	store := ctx.KVStore(k.storeKey)

	// delete subspace
	iterator := sdk.KVStorePrefixIterator(store, TendermintUpdatesKey)
	for ; iterator.Valid(); iterator.Next() {
		store.Delete(iterator.Key())
	}
	iterator.Close()
}

//___________________________________________________________________________

// perfom all the nessisary steps for when a validator changes its power
// updates all validator stores as well as tendermint update store
// may kick out validators if new validator is entering the bonded validator group
func (k Keeper) UpdateValidator(ctx sdk.Context, validator types.Validator) types.Validator {
	store := ctx.KVStore(k.storeKey)
	pool := k.GetPool(ctx)
	ownerAddr := validator.Owner

	// always update the main list ordered by owner address before exiting
	defer func() {
		bz := k.cdc.MustMarshalBinary(validator)
		store.Set(GetValidatorKey(ownerAddr), bz)
	}()

	// retrieve the old validator record
	oldValidator, oldFound := k.GetValidator(ctx, ownerAddr)

	if validator.Revoked && oldValidator.Status() == sdk.Bonded {
		validator = k.unbondValidator(ctx, validator)

		// need to also clear the cliff validator spot because the revoke has
		// opened up a new spot which will be filled when
		// updateValidatorsBonded is called
		k.clearCliffValidator(ctx)
	}

	powerIncreasing := false
	if oldFound && oldValidator.PoolShares.Bonded().LT(validator.PoolShares.Bonded()) {
		powerIncreasing = true
	}

	// if already a validator, copy the old block height and counter, else set them
	if oldFound && oldValidator.Status() == sdk.Bonded {
		validator.BondHeight = oldValidator.BondHeight
		validator.BondIntraTxCounter = oldValidator.BondIntraTxCounter
	} else {
		validator.BondHeight = ctx.BlockHeight()
		counter := k.GetIntraTxCounter(ctx)
		validator.BondIntraTxCounter = counter
		k.SetIntraTxCounter(ctx, counter+1)
	}

	// update the list ordered by voting power
	if oldFound {
		store.Delete(GetValidatorsByPowerIndexKey(oldValidator, pool))
	}
	valPower := GetValidatorsByPowerIndexKey(validator, pool)
	store.Set(valPower, validator.Owner)

	// efficiency case:
	// if already bonded and power increasing only need to update tendermint
	if powerIncreasing && !validator.Revoked && oldValidator.Status() == sdk.Bonded {
		bz := k.cdc.MustMarshalBinary(validator.ABCIValidator(k.cdc))
		store.Set(GetTendermintUpdatesKey(ownerAddr), bz)
		return validator
	}

	// efficiency case:
	// if was unbonded/or is a new validator - and the new power is less than the cliff validator
	cliffPower := k.getCliffValidatorPower(ctx)
	if cliffPower != nil &&
		(!oldFound || (oldFound && oldValidator.Status() == sdk.Unbonded)) &&
		bytes.Compare(valPower, cliffPower) == -1 { //(valPower < cliffPower
		return validator
	}

	// update the validator set for this validator
	updatedVal := k.UpdateBondedValidators(ctx, validator)
	if updatedVal.Owner != nil { // updates to validator occurred  to be updated
		validator = updatedVal
	}
	return validator
}

// Update the validator group and kick out any old validators. In addition this
// function adds (or doesn't add) a validator which has updated its bonded
// tokens to the validator group. -> this validator is specified through the
// updatedValidatorAddr term.
//
// The correct subset is retrieved by iterating through an index of the
// validators sorted by power, stored using the ValidatorsByPowerIndexKey.
// Simultaneously the current validator records are updated in store with the
// ValidatorsBondedIndexKey. This store is used to determine if a validator is a
// validator without needing to iterate over the subspace as we do in
// GetValidators.
//
// Optionally also return the validator from a retrieve address if the validator has been bonded
func (k Keeper) UpdateBondedValidators(ctx sdk.Context,
	newValidator types.Validator) (updatedVal types.Validator) {

	store := ctx.KVStore(k.storeKey)

	kickCliffValidator := false
	oldCliffValidatorAddr := k.getCliffValidator(ctx)

	// add the actual validator power sorted store
	maxValidators := k.GetParams(ctx).MaxValidators
	iterator := sdk.KVStoreReversePrefixIterator(store, ValidatorsByPowerIndexKey) // largest to smallest
	bondedValidatorsCount := 0
	var validator types.Validator
	for {
		if !iterator.Valid() || bondedValidatorsCount > int(maxValidators-1) {

			// TODO benchmark if we should read the current power and not write if it's the same
			if bondedValidatorsCount == int(maxValidators) { // is cliff validator
				k.setCliffValidator(ctx, validator, k.GetPool(ctx))
			}
			break
		}

		// either retrieve the original validator from the store, or under the
		// situation that this is the "new validator" just use the validator
		// provided because it has not yet been updated in the main validator
		// store
		ownerAddr := iterator.Value()
		if bytes.Equal(ownerAddr, newValidator.Owner) {
			validator = newValidator
		} else {
			var found bool
			validator, found = k.GetValidator(ctx, ownerAddr)
			if !found {
				panic(fmt.Sprintf("validator record not found for address: %v\n", ownerAddr))
			}
		}

		// if not previously a validator (and unrevoked),
		// kick the cliff validator / bond this new validator
		if validator.Status() != sdk.Bonded && !validator.Revoked {
			kickCliffValidator = true

			validator = k.bondValidator(ctx, validator)
			if bytes.Equal(ownerAddr, newValidator.Owner) {
				updatedVal = validator
			}
		}

		if validator.Revoked && validator.Status() == sdk.Bonded {
			panic(fmt.Sprintf("revoked validator cannot be bonded, address: %v\n", ownerAddr))
		} else {
			bondedValidatorsCount++
		}

		iterator.Next()
	}
	iterator.Close()

	// perform the actual kicks
	if oldCliffValidatorAddr != nil && kickCliffValidator {
		validator, found := k.GetValidator(ctx, oldCliffValidatorAddr)
		if !found {
			panic(fmt.Sprintf("validator record not found for address: %v\n", oldCliffValidatorAddr))
		}
		k.unbondValidator(ctx, validator)
	}

	return
}

// full update of the bonded validator set, many can be added/kicked
func (k Keeper) UpdateBondedValidatorsFull(ctx sdk.Context) {

	store := ctx.KVStore(k.storeKey)

	// clear the current validators store, add to the ToKickOut temp store
	toKickOut := make(map[string]byte)
	iterator := sdk.KVStorePrefixIterator(store, ValidatorsBondedIndexKey)
	for ; iterator.Valid(); iterator.Next() {
		ownerAddr := iterator.Value()
		toKickOut[string(ownerAddr)] = 0 // set anything
	}
	iterator.Close()

	// add the actual validator power sorted store
	maxValidators := k.GetParams(ctx).MaxValidators
	iterator = sdk.KVStoreReversePrefixIterator(store, ValidatorsByPowerIndexKey) // largest to smallest
	bondedValidatorsCount := 0
	var validator types.Validator
	for {
		if !iterator.Valid() || bondedValidatorsCount > int(maxValidators-1) {

			if bondedValidatorsCount == int(maxValidators) { // is cliff validator
				k.setCliffValidator(ctx, validator, k.GetPool(ctx))
			}
			break
		}

		// either retrieve the original validator from the store,
		// or under the situation that this is the "new validator" just
		// use the validator provided because it has not yet been updated
		// in the main validator store
		ownerAddr := iterator.Value()
		var found bool
		validator, found = k.GetValidator(ctx, ownerAddr)
		if !found {
			panic(fmt.Sprintf("validator record not found for address: %v\n", ownerAddr))
		}

		_, found = toKickOut[string(ownerAddr)]
		if found {
			delete(toKickOut, string(ownerAddr))
		} else {

			// if it wasn't in the toKickOut group it means
			// this wasn't a previously a validator, therefor
			// update the validator to enter the validator group
			validator = k.bondValidator(ctx, validator)
		}

		if validator.Revoked && validator.Status() == sdk.Bonded {
			panic(fmt.Sprintf("revoked validator cannot be bonded, address: %v\n", ownerAddr))
		} else {
			bondedValidatorsCount++
		}

		iterator.Next()
	}
	iterator.Close()

	// perform the actual kicks
	for key := range toKickOut {
		ownerAddr := []byte(key)
		validator, found := k.GetValidator(ctx, ownerAddr)
		if !found {
			panic(fmt.Sprintf("validator record not found for address: %v\n", ownerAddr))
		}
		k.unbondValidator(ctx, validator)
	}
	return
}

// perform all the store operations for when a validator status becomes unbonded
func (k Keeper) unbondValidator(ctx sdk.Context, validator types.Validator) types.Validator {

	store := ctx.KVStore(k.storeKey)
	pool := k.GetPool(ctx)

	// sanity check
	if validator.Status() == sdk.Unbonded {
		panic(fmt.Sprintf("should not already be unbonded,  validator: %v\n", validator))
	}

	// set the status
	validator, pool = validator.UpdateStatus(pool, sdk.Unbonded)
	k.SetPool(ctx, pool)

	// save the now unbonded validator record
	bzVal := k.cdc.MustMarshalBinary(validator)
	store.Set(GetValidatorKey(validator.Owner), bzVal)

	// add to accumulated changes for tendermint
	bzABCI := k.cdc.MustMarshalBinary(validator.ABCIValidatorZero(k.cdc))
	store.Set(GetTendermintUpdatesKey(validator.Owner), bzABCI)

	// also remove from the Bonded types.Validators Store
	store.Delete(GetValidatorsBondedIndexKey(validator.Owner))
	return validator
}

// perform all the store operations for when a validator status becomes bonded
func (k Keeper) bondValidator(ctx sdk.Context, validator types.Validator) types.Validator {

	store := ctx.KVStore(k.storeKey)
	pool := k.GetPool(ctx)

	// sanity check
	if validator.Status() == sdk.Bonded {
		panic(fmt.Sprintf("should not already be bonded, validator: %v\n", validator))
	}

	// set the status
	validator, pool = validator.UpdateStatus(pool, sdk.Bonded)
	k.SetPool(ctx, pool)

	// save the now bonded validator record to the three referenced stores
	bzVal := k.cdc.MustMarshalBinary(validator)
	store.Set(GetValidatorKey(validator.Owner), bzVal)
	store.Set(GetValidatorsBondedIndexKey(validator.Owner), validator.Owner)

	// add to accumulated changes for tendermint
	bzABCI := k.cdc.MustMarshalBinary(validator.ABCIValidator(k.cdc))
	store.Set(GetTendermintUpdatesKey(validator.Owner), bzABCI)

	return validator
}

// remove the validator record and associated indexes
func (k Keeper) RemoveValidator(ctx sdk.Context, address sdk.Address) {

	// first retrieve the old validator record
	validator, found := k.GetValidator(ctx, address)
	if !found {
		return
	}

	// delete the old validator record
	store := ctx.KVStore(k.storeKey)
	pool := k.GetPool(ctx)
	store.Delete(GetValidatorKey(address))
	store.Delete(GetValidatorByPubKeyIndexKey(validator.PubKey))
	store.Delete(GetValidatorsByPowerIndexKey(validator, pool))

	// delete from the current and power weighted validator groups if the validator
	// is bonded - and add validator with zero power to the validator updates
	if store.Get(GetValidatorsBondedIndexKey(validator.Owner)) == nil {
		return
	}
	store.Delete(GetValidatorsBondedIndexKey(validator.Owner))

	bz := k.cdc.MustMarshalBinary(validator.ABCIValidatorZero(k.cdc))
	store.Set(GetTendermintUpdatesKey(address), bz)
}

//__________________________________________________________________________

// get the current validator on the cliff
func (k Keeper) getCliffValidator(ctx sdk.Context) []byte {
	store := ctx.KVStore(k.storeKey)
	return store.Get(ValidatorCliffIndexKey)
}

// get the current power of the validator on the cliff
func (k Keeper) getCliffValidatorPower(ctx sdk.Context) []byte {
	store := ctx.KVStore(k.storeKey)
	return store.Get(ValidatorPowerCliffKey)
}

// set the current validator and power of the validator on the cliff
func (k Keeper) setCliffValidator(ctx sdk.Context, validator types.Validator, pool types.Pool) {
	store := ctx.KVStore(k.storeKey)
	bz := GetValidatorsByPowerIndexKey(validator, pool)
	store.Set(ValidatorPowerCliffKey, bz)
	store.Set(ValidatorCliffIndexKey, validator.Owner)
}

// clear the current validator and power of the validator on the cliff
func (k Keeper) clearCliffValidator(ctx sdk.Context) {
	store := ctx.KVStore(k.storeKey)
	store.Delete(ValidatorPowerCliffKey)
	store.Delete(ValidatorCliffIndexKey)
}
