package simulation

import (
	"math"
	"math/rand"
	"time"

	sdkmath "cosmossdk.io/math"

	"git.cw.tr/mukan-network/mukan-sdk/baseapp"
	"git.cw.tr/mukan-network/mukan-sdk/client"
	simtestutil "git.cw.tr/mukan-network/mukan-sdk/testutil/sims"
	sdk "git.cw.tr/mukan-network/mukan-sdk/types"
	simtypes "git.cw.tr/mukan-network/mukan-sdk/types/simulation"
	"git.cw.tr/mukan-network/mukan-sdk/x/gov/keeper"
	"git.cw.tr/mukan-network/mukan-sdk/x/gov/types"
	v1 "git.cw.tr/mukan-network/mukan-sdk/x/gov/types/v1"
	"git.cw.tr/mukan-network/mukan-sdk/x/simulation"
)

// Governance message types and routes
// will be removed in the future
var (
	TypeMsgDeposit        = sdk.MsgTypeURL(&v1.MsgDeposit{})
	TypeMsgVote           = sdk.MsgTypeURL(&v1.MsgVote{})
	TypeMsgVoteWeighted   = sdk.MsgTypeURL(&v1.MsgVoteWeighted{})
	TypeMsgSubmitProposal = sdk.MsgTypeURL(&v1.MsgSubmitProposal{})
	TypeMsgCancelProposal = sdk.MsgTypeURL(&v1.MsgCancelProposal{})
)

// Simulation operation weights constants
// will be removed in the future
const (
	OpWeightMsgDeposit        = "op_weight_msg_deposit"
	OpWeightMsgVote           = "op_weight_msg_vote"
	OpWeightMsgVoteWeighted   = "op_weight_msg_weighted_vote"
	OpWeightMsgCancelProposal = "op_weight_msg_cancel_proposal"

	DefaultWeightMsgDeposit        = 100
	DefaultWeightMsgVote           = 67
	DefaultWeightMsgVoteWeighted   = 33
	DefaultWeightTextProposal      = 5
	DefaultWeightMsgCancelProposal = 5
)

// WeightedOperations returns all the operations from the module with their respective weights
// will be removed in the future in favor of msg factory
func WeightedOperations(
	appParams simtypes.AppParams,
	txGen client.TxConfig,
	ak types.AccountKeeper,
	bk types.BankKeeper,
	k *keeper.Keeper,
	wMsgs []simtypes.WeightedProposalMsg,
	wContents []simtypes.WeightedProposalContent, //nolint:staticcheck // used for legacy testing
) simulation.WeightedOperations {
	var (
		weightMsgDeposit        int
		weightMsgVote           int
		weightMsgVoteWeighted   int
		weightMsgCancelProposal int
	)

	appParams.GetOrGenerate(OpWeightMsgDeposit, &weightMsgDeposit, nil,
		func(_ *rand.Rand) {
			weightMsgDeposit = DefaultWeightMsgDeposit
		},
	)

	appParams.GetOrGenerate(OpWeightMsgVote, &weightMsgVote, nil,
		func(_ *rand.Rand) {
			weightMsgVote = DefaultWeightMsgVote
		},
	)

	appParams.GetOrGenerate(OpWeightMsgVoteWeighted, &weightMsgVoteWeighted, nil,
		func(_ *rand.Rand) {
			weightMsgVoteWeighted = DefaultWeightMsgVoteWeighted
		},
	)

	appParams.GetOrGenerate(OpWeightMsgCancelProposal, &weightMsgCancelProposal, nil,
		func(_ *rand.Rand) {
			weightMsgCancelProposal = DefaultWeightMsgCancelProposal
		},
	)

	// generate the weighted operations for the proposal msgs
	var wProposalOps simulation.WeightedOperations
	for _, wMsg := range wMsgs {
		var weight int
		appParams.GetOrGenerate(wMsg.AppParamsKey(), &weight, nil,
			func(_ *rand.Rand) { weight = wMsg.DefaultWeight() },
		)

		wProposalOps = append(
			wProposalOps,
			simulation.NewWeightedOperation(
				weight,
				SimulateMsgSubmitProposal(txGen, ak, bk, k, wMsg.MsgSimulatorFn()),
			),
		)
	}

	// generate the weighted operations for the proposal contents
	var wLegacyProposalOps simulation.WeightedOperations
	for _, wContent := range wContents {
		var weight int
		appParams.GetOrGenerate(wContent.AppParamsKey(), &weight, nil,
			func(_ *rand.Rand) { weight = wContent.DefaultWeight() },
		)

		wLegacyProposalOps = append(
			wLegacyProposalOps,
			simulation.NewWeightedOperation(
				weight,
				SimulateMsgSubmitLegacyProposal(txGen, ak, bk, k, wContent.ContentSimulatorFn()),
			),
		)
	}
	state := NewSharedState()
	wGovOps := simulation.WeightedOperations{
		simulation.NewWeightedOperation(
			weightMsgDeposit,
			simulateMsgDeposit(txGen, ak, bk, k, state),
		),
		simulation.NewWeightedOperation(
			weightMsgVote,
			simulateMsgVote(txGen, ak, bk, k, state),
		),
		simulation.NewWeightedOperation(
			weightMsgVoteWeighted,
			simulateMsgVoteWeighted(txGen, ak, bk, k, state),
		),
		simulation.NewWeightedOperation(
			weightMsgCancelProposal,
			SimulateMsgCancelProposal(txGen, ak, bk, k),
		),
	}

	return append(wGovOps, append(wProposalOps, wLegacyProposalOps...)...)
}

// SimulateMsgSubmitProposal simulates creating a msg Submit Proposal
// voting on the proposal, and subsequently slashing the proposal. It is implemented using
// future operations.
// will be removed in the future in favor of msg factory
func SimulateMsgSubmitProposal(
	txGen client.TxConfig,
	ak types.AccountKeeper,
	bk types.BankKeeper,
	k *keeper.Keeper,
	msgSim simtypes.MsgSimulatorFn,
) simtypes.Operation {
	return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
	) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
		msgs := []sdk.Msg{}
		proposalMsg := msgSim(r, ctx, accs)
		if proposalMsg != nil {
			msgs = append(msgs, proposalMsg)
		}

		return simulateMsgSubmitProposal(txGen, ak, bk, k, msgs)(r, app, ctx, accs, chainID)
	}
}

// SimulateMsgSubmitLegacyProposal simulates creating a msg Submit Proposal
// voting on the proposal, and subsequently slashing the proposal. It is implemented using
// future operations.
// will be removed in the future in favor of msg factory
func SimulateMsgSubmitLegacyProposal(
	txGen client.TxConfig,
	ak types.AccountKeeper,
	bk types.BankKeeper,
	k *keeper.Keeper,
	contentSim simtypes.ContentSimulatorFn, //nolint:staticcheck // used for legacy testing
) simtypes.Operation {
	return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string,
	) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
		// 1) submit proposal now
		content := contentSim(r, ctx, accs)
		if content == nil {
			return simtypes.NoOpMsg(types.ModuleName, TypeMsgSubmitProposal, "content is nil"), nil, nil
		}

		govacc := k.GetGovernanceAccount(ctx)
		contentMsg, err := v1.NewLegacyContent(content, govacc.GetAddress().String())
		if err != nil {
			return simtypes.NoOpMsg(types.ModuleName, TypeMsgSubmitProposal, "error converting legacy content into proposal message"), nil, err
		}

		return simulateMsgSubmitProposal(txGen, ak, bk, k, []sdk.Msg{contentMsg})(r, app, ctx, accs, chainID)
	}
}

func simulateMsgSubmitProposal(
	txGen client.TxConfig,
	ak types.AccountKeeper,
	bk types.BankKeeper,
	k *keeper.Keeper,
	proposalMsgs []sdk.Msg,
) simtypes.Operation {
	// The states are:
	// column 1: All validators vote
	// column 2: 90% vote
	// column 3: 75% vote
	// column 4: 40% vote
	// column 5: 15% vote
	// column 6: no one votes
	// All columns sum to 100 for simplicity, values chosen by @valardragon semi-arbitrarily,
	// feel free to change.
	numVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{
		{20, 10, 0, 0, 0, 0},
		{55, 50, 20, 10, 0, 0},
		{25, 25, 30, 25, 30, 15},
		{0, 15, 30, 25, 30, 30},
		{0, 0, 20, 30, 30, 30},
		{0, 0, 0, 10, 10, 25},
	})

	statePercentageArray := []float64{1, .9, .75, .4, .15, 0}
	curNumVotesState := 1

	return func(
		r *rand.Rand,
		app *baseapp.BaseApp,
		ctx sdk.Context,
		accs []simtypes.Account,
		chainID string,
	) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
		simAccount, _ := simtypes.RandomAcc(r, accs)
		expedited := r.Intn(2) == 0
		deposit, skip, err := randomDeposit(r, ctx, ak, bk, k, simAccount.Address, true, expedited)
		switch {
		case skip:
			return simtypes.NoOpMsg(types.ModuleName, TypeMsgSubmitProposal, "skip deposit"), nil, nil
		case err != nil:
			return simtypes.NoOpMsg(types.ModuleName, TypeMsgSubmitProposal, "unable to generate deposit"), nil, err
		}

		msg, err := v1.NewMsgSubmitProposal(
			proposalMsgs,
			deposit,
			simAccount.Address.String(),
			simtypes.RandStringOfLength(r, 100),
			simtypes.RandStringOfLength(r, 100),
			simtypes.RandStringOfLength(r, 100),
			expedited,
		)
		if err != nil {
			return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to generate a submit proposal msg"), nil, err
		}

		account := ak.GetAccount(ctx, simAccount.Address)
		tx, err := simtestutil.GenSignedMockTx(
			r,
			txGen,
			[]sdk.Msg{msg},
			sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)},
			simtestutil.DefaultGenTxGas,
			chainID,
			[]uint64{account.GetAccountNumber()},
			[]uint64{account.GetSequence()},
			simAccount.PrivKey,
		)
		if err != nil {
			return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to generate mock tx"), nil, err
		}

		_, _, err = app.SimDeliver(txGen.TxEncoder(), tx)
		if err != nil {
			return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to deliver tx"), nil, err
		}

		opMsg := simtypes.NewOperationMsg(msg, true, "")

		// get the submitted proposal ID
		proposalID, err := k.ProposalID.Peek(ctx)
		if err != nil {
			return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to generate proposalID"), nil, err
		}

		// 2) Schedule operations for votes
		// 2.1) first pick a number of people to vote.
		curNumVotesState = numVotesTransitionMatrix.NextState(r, curNumVotesState)
		numVotes := int(math.Ceil(float64(len(accs)) * statePercentageArray[curNumVotesState]))

		// 2.2) select who votes and when
		whoVotes := r.Perm(len(accs))

		// didntVote := whoVotes[numVotes:]
		whoVotes = whoVotes[:numVotes]
		params, _ := k.Params.Get(ctx)
		votingPeriod := params.VotingPeriod
		s := NewSharedState()
		fops := make([]simtypes.FutureOperation, numVotes+1)
		for i := range numVotes {
			whenVote := ctx.BlockHeader().Time.Add(time.Duration(r.Int63n(int64(votingPeriod.Seconds()))) * time.Second)
			fops[i] = simtypes.FutureOperation{
				BlockTime: whenVote,
				Op:        operationSimulateMsgVote(txGen, ak, bk, k, accs[whoVotes[i]], int64(proposalID), s),
			}
		}

		return opMsg, fops, nil
	}
}

// SimulateMsgDeposit generates a MsgDeposit with random values.
// migrate to the msg factories instead, this method will be removed in the future
func SimulateMsgDeposit(
	txGen client.TxConfig,
	ak types.AccountKeeper,
	bk types.BankKeeper,
	k *keeper.Keeper,
) simtypes.Operation {
	return simulateMsgDeposit(txGen, ak, bk, k, NewSharedState())
}

func simulateMsgDeposit(
	txGen client.TxConfig,
	ak types.AccountKeeper,
	bk types.BankKeeper,
	k *keeper.Keeper,
	s *SharedState,
) simtypes.Operation {
	return func(
		r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
		accs []simtypes.Account, chainID string,
	) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
		simAccount, _ := simtypes.RandomAcc(r, accs)
		proposalID, ok := randomProposalID(r, k, ctx, v1.StatusDepositPeriod, s)
		if !ok {
			return simtypes.NoOpMsg(types.ModuleName, TypeMsgDeposit, "unable to generate proposalID"), nil, nil
		}

		p, err := k.Proposals.Get(ctx, proposalID)
		if err != nil {
			return simtypes.NoOpMsg(types.ModuleName, TypeMsgDeposit, "unable to get proposal"), nil, err
		}

		deposit, skip, err := randomDeposit(r, ctx, ak, bk, k, simAccount.Address, false, p.Expedited)
		switch {
		case skip:
			return simtypes.NoOpMsg(types.ModuleName, TypeMsgDeposit, "skip deposit"), nil, nil
		case err != nil:
			return simtypes.NoOpMsg(types.ModuleName, TypeMsgDeposit, "unable to generate deposit"), nil, err
		}

		msg := v1.NewMsgDeposit(simAccount.Address, proposalID, deposit)

		account := ak.GetAccount(ctx, simAccount.Address)
		spendable := bk.SpendableCoins(ctx, account.GetAddress())

		var fees sdk.Coins
		coins, hasNeg := spendable.SafeSub(deposit...)
		if !hasNeg {
			fees, err = simtypes.RandomFees(r, ctx, coins)
			if err != nil {
				return simtypes.NoOpMsg(types.ModuleName, sdk.MsgTypeURL(msg), "unable to generate fees"), nil, err
			}
		}

		txCtx := simulation.OperationInput{
			R:             r,
			App:           app,
			TxGen:         txGen,
			Cdc:           nil,
			Msg:           msg,
			Context:       ctx,
			SimAccount:    simAccount,
			AccountKeeper: ak,
			ModuleName:    types.ModuleName,
		}

		return simulation.GenAndDeliverTx(txCtx, fees)
	}
}

// SimulateMsgVote generates a MsgVote with random values.
//
// Deprecated: this is an internal method and will be removed
func SimulateMsgVote(
	txGen client.TxConfig,
	ak types.AccountKeeper,
	bk types.BankKeeper,
	k *keeper.Keeper,
) simtypes.Operation {
	return simulateMsgVote(txGen, ak, bk, k, NewSharedState())
}

func simulateMsgVote(
	txGen client.TxConfig,
	ak types.AccountKeeper,
	bk types.BankKeeper,
	k *keeper.Keeper,
	s *SharedState,
) simtypes.Operation {
	return operationSimulateMsgVote(txGen, ak, bk, k, simtypes.Account{}, -1, s)
}

func operationSimulateMsgVote(
	txGen client.TxConfig,
	ak types.AccountKeeper,
	bk types.BankKeeper,
	k *keeper.Keeper,
	simAccount simtypes.Account,
	proposalIDInt int64,
	s *SharedState,
) simtypes.Operation {
	return func(
		r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
		accs []simtypes.Account, chainID string,
	) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
		if simAccount.Equals(simtypes.Account{}) {
			simAccount, _ = simtypes.RandomAcc(r, accs)
		}

		var proposalID uint64

		switch {
		case proposalIDInt < 0:
			var ok bool
			proposalID, ok = randomProposalID(r, k, ctx, v1.StatusVotingPeriod, s)
			if !ok {
				return simtypes.NoOpMsg(types.ModuleName, TypeMsgVote, "unable to generate proposalID"), nil, nil
			}
		default:
			proposalID = uint64(proposalIDInt)
		}

		option := randomVotingOption(r)
		msg := v1.NewMsgVote(simAccount.Address, proposalID, option, "")

		account := ak.GetAccount(ctx, simAccount.Address)
		spendable := bk.SpendableCoins(ctx, account.GetAddress())

		txCtx := simulation.OperationInput{
			R:               r,
			App:             app,
			TxGen:           txGen,
			Cdc:             nil,
			Msg:             msg,
			Context:         ctx,
			SimAccount:      simAccount,
			AccountKeeper:   ak,
			Bankkeeper:      bk,
			ModuleName:      types.ModuleName,
			CoinsSpentInMsg: spendable,
		}

		return simulation.GenAndDeliverTxWithRandFees(txCtx)
	}
}

// SimulateMsgVoteWeighted generates a MsgVoteWeighted with random values.
// will be removed in the future in favor of msg factory
func SimulateMsgVoteWeighted(
	txGen client.TxConfig,
	ak types.AccountKeeper,
	bk types.BankKeeper,
	k *keeper.Keeper,
) simtypes.Operation {
	return simulateMsgVoteWeighted(txGen, ak, bk, k, NewSharedState())
}

func simulateMsgVoteWeighted(
	txGen client.TxConfig,
	ak types.AccountKeeper,
	bk types.BankKeeper,
	k *keeper.Keeper,
	s *SharedState,
) simtypes.Operation {
	return operationSimulateMsgVoteWeighted(txGen, ak, bk, k, simtypes.Account{}, -1, s)
}

func operationSimulateMsgVoteWeighted(
	txGen client.TxConfig,
	ak types.AccountKeeper,
	bk types.BankKeeper,
	k *keeper.Keeper,
	simAccount simtypes.Account,
	proposalIDInt int64,
	s *SharedState,
) simtypes.Operation {
	return func(
		r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
		accs []simtypes.Account, chainID string,
	) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
		if simAccount.Equals(simtypes.Account{}) {
			simAccount, _ = simtypes.RandomAcc(r, accs)
		}

		var proposalID uint64

		switch {
		case proposalIDInt < 0:
			var ok bool
			proposalID, ok = randomProposalID(r, k, ctx, v1.StatusVotingPeriod, s)
			if !ok {
				return simtypes.NoOpMsg(types.ModuleName, TypeMsgVoteWeighted, "unable to generate proposalID"), nil, nil
			}
		default:
			proposalID = uint64(proposalIDInt)
		}

		options := randomWeightedVotingOptions(r)
		msg := v1.NewMsgVoteWeighted(simAccount.Address, proposalID, options, "")

		account := ak.GetAccount(ctx, simAccount.Address)
		spendable := bk.SpendableCoins(ctx, account.GetAddress())

		txCtx := simulation.OperationInput{
			R:               r,
			App:             app,
			TxGen:           txGen,
			Cdc:             nil,
			Msg:             msg,
			Context:         ctx,
			SimAccount:      simAccount,
			AccountKeeper:   ak,
			Bankkeeper:      bk,
			ModuleName:      types.ModuleName,
			CoinsSpentInMsg: spendable,
		}

		return simulation.GenAndDeliverTxWithRandFees(txCtx)
	}
}

// SimulateMsgCancelProposal generates a MsgCancelProposal.
// will be removed in the future in favor of msg factory
func SimulateMsgCancelProposal(
	txGen client.TxConfig,
	ak types.AccountKeeper,
	bk types.BankKeeper,
	k *keeper.Keeper,
) simtypes.Operation {
	return func(
		r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
		accs []simtypes.Account, chainID string,
	) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
		simAccount := accs[0]
		proposal := randomProposal(r, k, ctx)
		if proposal == nil {
			return simtypes.NoOpMsg(types.ModuleName, TypeMsgCancelProposal, "no proposals found"), nil, nil
		}

		if proposal.Proposer != simAccount.Address.String() {
			return simtypes.NoOpMsg(types.ModuleName, TypeMsgCancelProposal, "invalid proposer"), nil, nil
		}

		if (proposal.Status != v1.StatusDepositPeriod) && (proposal.Status != v1.StatusVotingPeriod) {
			return simtypes.NoOpMsg(types.ModuleName, TypeMsgCancelProposal, "invalid proposal status"), nil, nil
		}

		account := ak.GetAccount(ctx, simAccount.Address)
		spendable := bk.SpendableCoins(ctx, account.GetAddress())

		msg := v1.NewMsgCancelProposal(proposal.Id, account.GetAddress().String())

		txCtx := simulation.OperationInput{
			R:               r,
			App:             app,
			TxGen:           txGen,
			Cdc:             nil,
			Msg:             msg,
			Context:         ctx,
			SimAccount:      simAccount,
			AccountKeeper:   ak,
			Bankkeeper:      bk,
			ModuleName:      types.ModuleName,
			CoinsSpentInMsg: spendable,
		}

		return simulation.GenAndDeliverTxWithRandFees(txCtx)
	}
}

// Pick a random deposit with a random denomination with a
// deposit amount between (0, min(balance, minDepositAmount))
// This is to simulate multiple users depositing to get the
// proposal above the minimum deposit amount
func randomDeposit(
	r *rand.Rand,
	ctx sdk.Context,
	ak types.AccountKeeper,
	bk types.BankKeeper,
	k *keeper.Keeper,
	addr sdk.AccAddress,
	useMinAmount bool,
	expedited bool,
) (deposit sdk.Coins, skip bool, err error) {
	account := ak.GetAccount(ctx, addr)
	spendable := bk.SpendableCoins(ctx, account.GetAddress())

	if spendable.Empty() {
		return nil, true, nil // skip
	}

	params, _ := k.Params.Get(ctx)
	minDeposit := params.MinDeposit
	if expedited {
		minDeposit = params.ExpeditedMinDeposit
	}
	denomIndex := r.Intn(len(minDeposit))
	denom := minDeposit[denomIndex].Denom

	spendableBalance := spendable.AmountOf(denom)
	if spendableBalance.IsZero() {
		return nil, true, nil
	}

	minDepositAmount := minDeposit[denomIndex].Amount

	minDepositRatio, err := sdkmath.LegacyNewDecFromStr(params.GetMinDepositRatio())
	if err != nil {
		return nil, false, err
	}

	threshold := minDepositAmount.ToLegacyDec().Mul(minDepositRatio).TruncateInt()

	minAmount := sdkmath.ZeroInt()
	if useMinAmount {
		minDepositPercent, err := sdkmath.LegacyNewDecFromStr(params.MinInitialDepositRatio)
		if err != nil {
			return nil, false, err
		}

		minAmount = sdkmath.LegacyNewDecFromInt(minDepositAmount).Mul(minDepositPercent).TruncateInt()
	}

	amount, err := simtypes.RandPositiveInt(r, minDepositAmount.Sub(minAmount))
	if err != nil {
		return nil, false, err
	}
	amount = amount.Add(minAmount)

	if amount.GT(spendableBalance) || amount.LT(threshold) {
		return nil, true, nil
	}

	return sdk.Coins{sdk.NewCoin(denom, amount)}, false, nil
}

// randomProposal returns a random proposal stored in state
func randomProposal(r *rand.Rand, k *keeper.Keeper, ctx sdk.Context) *v1.Proposal {
	var proposals []*v1.Proposal
	err := k.Proposals.Walk(ctx, nil, func(key uint64, value v1.Proposal) (stop bool, err error) {
		proposals = append(proposals, &value)
		return false, nil
	})
	if err != nil {
		panic(err)
	}
	if len(proposals) == 0 {
		return nil
	}
	randomIndex := r.Intn(len(proposals))
	return proposals[randomIndex]
}
