package keeper

import (
	"fmt"

	sdk "github.com/cosmos/cosmos-sdk/types"

	govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
	stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

	wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"
)

// DefaultContractAddr is the wasm contract address generated by code ID 1 and
// instance ID 1.
//
// In other words, the first ever contract to be deployed on this chain will
// necessarily have this address.
//
// Why don't we make this a configurable parameter in the module params?
// Because doing so involves messing with the vanilla gov module's params type,
// which breaks a bunch of code and imo, isn't worth it.
var DefaultContractAddr = wasmkeeper.BuildContractAddressClassic(1, 1)

// Tally iterates over the votes and updates the tally of a proposal based on
// the voting power of the voters.
//
// NOTE: here the voting power of a user is defined as: amount of MARS tokens
// staked + amount locked in vesting.
func (k Keeper) Tally(ctx sdk.Context, proposal govv1.Proposal) (passes bool, burnDeposits bool, tallyResults govv1.TallyResult) {
	results := make(map[govv1.VoteOption]sdk.Dec)
	results[govv1.OptionYes] = sdk.ZeroDec()
	results[govv1.OptionAbstain] = sdk.ZeroDec()
	results[govv1.OptionNo] = sdk.ZeroDec()
	results[govv1.OptionNoWithVeto] = sdk.ZeroDec()

	// fetch all currently bonded validators
	currValidators := make(map[string]govv1.ValidatorGovInfo)
	k.stakingKeeper.IterateBondedValidatorsByPower(ctx, func(index int64, validator stakingtypes.ValidatorI) (stop bool) {
		currValidators[validator.GetOperator().String()] = govv1.NewValidatorGovInfo(
			validator.GetOperator(),
			validator.GetBondedTokens(),
			validator.GetDelegatorShares(),
			sdk.ZeroDec(),
			govv1.WeightedVoteOptions{},
		)

		return false
	})

	// fetch all tokens locked in the vesting contract
	tokensInVesting, totalTokensInVesting := MustGetTokensInVesting(ctx, k.wasmKeeper, DefaultContractAddr)

	// total amount of tokens bonded with validators
	totalTokensBonded := k.stakingKeeper.TotalBondedTokens(ctx)

	// total amount of tokens that are eligible to vote in this poll; used to
	// determine quorum
	totalTokens := sdk.NewDecFromInt(totalTokensBonded.Add(totalTokensInVesting))

	// total amount of tokens that have voted in this poll; used to determine
	// whether the poll reaches quorum and the pass threshold
	totalTokensVoted := sdk.ZeroDec()

	// iterate through votes
	k.IterateVotes(ctx, proposal.Id, func(vote govv1.Vote) bool {
		voterAddr := sdk.MustAccAddressFromBech32(vote.Voter)

		// if validator, record its vote options in the map
		valAddrStr := sdk.ValAddress(voterAddr).String()
		if val, ok := currValidators[valAddrStr]; ok {
			val.Vote = vote.Options
			currValidators[valAddrStr] = val
		}

		votingPower := sdk.ZeroDec()

		// iterate over all delegations from voter, deduct from any delegated-to
		// validators
		//
		// there is no need to handle the special case that validator address
		// equal to voter address, because voter's voting power will tally again
		// even if there will deduct voter's voting power from validator
		k.stakingKeeper.IterateDelegations(ctx, voterAddr, func(index int64, delegation stakingtypes.DelegationI) (stop bool) {
			valAddrStr := delegation.GetValidatorAddr().String()

			if val, ok := currValidators[valAddrStr]; ok {
				val.DelegatorDeductions = val.DelegatorDeductions.Add(delegation.GetShares())
				currValidators[valAddrStr] = val

				votingPower = votingPower.Add(delegation.GetShares().MulInt(val.BondedTokens).Quo(val.DelegatorShares))
			}

			return false
		})

		// if the voter has tokens locked in vesting contract, add that to the
		// voting power
		if votingPowerInVesting, ok := tokensInVesting[vote.Voter]; ok {
			votingPower = votingPower.Add(sdk.NewDecFromInt(votingPowerInVesting))
		}

		incrementTallyResult(votingPower, vote.Options, results, &totalTokensVoted)
		k.deleteVote(ctx, vote.ProposalId, voterAddr)

		return false
	})

	// iterate over the validators again to tally their voting power
	for _, val := range currValidators {
		if len(val.Vote) == 0 {
			continue
		}

		sharesAfterDeductions := val.DelegatorShares.Sub(val.DelegatorDeductions)
		votingPower := sharesAfterDeductions.MulInt(val.BondedTokens).Quo(val.DelegatorShares)

		incrementTallyResult(votingPower, val.Vote, results, &totalTokensVoted)
	}

	tallyParams := k.GetTallyParams(ctx)
	tallyResults = govv1.NewTallyResultFromMap(results)

	quorum, err := sdk.NewDecFromStr(tallyParams.Quorum)
	if err != nil {
		panic(fmt.Sprintf("Invalid governance parameter quorum `%s`: %s", tallyParams.Quorum, err))
	}

	vetoThreshold, err := sdk.NewDecFromStr(tallyParams.VetoThreshold)
	if err != nil {
		panic(fmt.Sprintf("Invalid governance parameter vetoThreshold `%s`: %s", tallyParams.VetoThreshold, err))
	}

	passThreshold, err := sdk.NewDecFromStr(tallyParams.Threshold)
	if err != nil {
		panic(fmt.Sprintf("Invalid governance parameter passThreshold `%s`: %s", tallyParams.Threshold, err))
	}

	// if there is no staked coins, the proposal fails
	if k.stakingKeeper.TotalBondedTokens(ctx).IsZero() {
		return false, false, tallyResults
	}

	// if there is not enough quorum of votes, the proposal fails
	if totalTokensVoted.Quo(totalTokens).LT(quorum) {
		return false, false, tallyResults
	}

	// if everyone abstains, proposal fails
	if totalTokensVoted.Sub(results[govv1.OptionAbstain]).IsZero() {
		return false, false, tallyResults
	}

	// if more than 1/3 of voters veto, proposal fails, and deposit burned
	//
	// NOTE: here 1/3 is defined as 1/3 *of all votes*, including abstaining
	// votes. could it make more sense to instead define it as 1/3 *of all
	// non-abstaining votes*?
	if results[govv1.OptionNoWithVeto].Quo(totalTokensVoted).GT(vetoThreshold) {
		return false, true, tallyResults
	}

	// if no less than 1/2 of non-abstaining voters vote No, proposal fails
	if results[govv1.OptionNo].Quo(totalTokensVoted.Sub(results[govv1.OptionAbstain])).GTE(passThreshold) {
		return false, false, tallyResults
	}

	// otherwise, meaning more than 1/2 of non-abstaining voters vote Yes,
	// proposal passes
	return true, false, tallyResults
}

func incrementTallyResult(votingPower sdk.Dec, options []*govv1.WeightedVoteOption, results map[govv1.VoteOption]sdk.Dec, totalTokensVoted *sdk.Dec) {
	for _, option := range options {
		weight, err := sdk.NewDecFromStr(option.Weight)
		if err != nil {
			panic(fmt.Sprintf("Invalid vote weight `%s`: %s", option.Weight, err))
		}

		subPower := votingPower.Mul(weight)
		results[option.Option] = results[option.Option].Add(subPower)
	}

	*totalTokensVoted = totalTokensVoted.Add(votingPower)
}
