package simulation

import (
	"fmt"
	"math/rand"

	"github.com/cosmos/cosmos-sdk/baseapp"
	"github.com/cosmos/cosmos-sdk/codec"
	"github.com/cosmos/cosmos-sdk/simapp/helpers"
	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/cosmos/cosmos-sdk/x/simulation"
	simappparams "github.com/kava-labs/kava/app/params"

	"github.com/kava-labs/kava/x/issuance/keeper"
	"github.com/kava-labs/kava/x/issuance/types"
)

// Simulation operation weights constants
const (
	OpWeightMsgIssue  = "op_weight_msg_issue"
	OpWeightMsgRedeem = "op_weight_msg_redeem"
	OpWeightMsgBlock  = "op_weight_msg_block"
	OpWeightMsgPause  = "op_weight_msg_pause"
)

// WeightedOperations returns all the operations from the module with their respective weights
func WeightedOperations(
	appParams simulation.AppParams, cdc *codec.Codec, ak types.AccountKeeper, k keeper.Keeper,
) simulation.WeightedOperations {
	var (
		weightMsgIssue  int
		weightMsgReedem int
		weightMsgBlock  int
		weightMsgPause  int
	)

	appParams.GetOrGenerate(cdc, OpWeightMsgIssue, &weightMsgIssue, nil,
		func(_ *rand.Rand) {
			weightMsgIssue = simappparams.DefaultWeightMsgIssue
		},
	)

	appParams.GetOrGenerate(cdc, OpWeightMsgRedeem, &weightMsgReedem, nil,
		func(_ *rand.Rand) {
			weightMsgReedem = simappparams.DefaultWeightMsgRedeem
		},
	)

	appParams.GetOrGenerate(cdc, OpWeightMsgBlock, &weightMsgBlock, nil,
		func(_ *rand.Rand) {
			weightMsgBlock = simappparams.DefaultWeightMsgBlock
		},
	)

	appParams.GetOrGenerate(cdc, OpWeightMsgPause, &weightMsgPause, nil,
		func(_ *rand.Rand) {
			weightMsgPause = simappparams.DefaultWeightMsgPause
		},
	)

	return simulation.WeightedOperations{
		simulation.NewWeightedOperation(
			weightMsgIssue,
			SimulateMsgIssueTokens(ak, k),
		),
		simulation.NewWeightedOperation(
			weightMsgReedem,
			SimulateMsgRedeemTokens(ak, k),
		),
		simulation.NewWeightedOperation(
			weightMsgBlock,
			SimulateMsgBlockAddress(ak, k),
		),
		simulation.NewWeightedOperation(
			weightMsgPause,
			SimulateMsgPause(ak, k),
		),
	}
}

// SimulateMsgIssueTokens generates a MsgIssueTokens with random values
func SimulateMsgIssueTokens(ak types.AccountKeeper, k keeper.Keeper) simulation.Operation {
	return func(
		r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
	) (simulation.OperationMsg, []simulation.FutureOperation, error) {
		// shuffle the assets and get a random one
		assets := k.GetParams(ctx).Assets
		r.Shuffle(len(assets), func(i, j int) {
			assets[i], assets[j] = assets[j], assets[i]
		})
		asset := assets[0]

		if asset.Paused {
			return simulation.NoOpMsg(types.ModuleName), nil, nil
		}

		// make sure owner account exists
		ownerSimAcc, found := simulation.FindAccount(accs, asset.Owner)
		if !found {
			return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("asset owner not found: %s", asset)
		}

		// issue new tokens to the owner 50% of the time so we have funds to redeem
		ownerAcc := ak.GetAccount(ctx, asset.Owner)
		recipient := ownerAcc
		if r.Intn(2) == 0 {
			simAccount, _ := simulation.RandomAcc(r, accs)
			recipient = ak.GetAccount(ctx, simAccount.Address)
		}
		if recipient == nil {
			return simulation.NoOpMsg(types.ModuleName), nil, nil
		}
		for _, blockedAddr := range asset.BlockedAddresses {
			if recipient.GetAddress().Equals(blockedAddr) {
				return simulation.NoOpMsg(types.ModuleName), nil, nil
			}
		}
		randomAmount := simulation.RandIntBetween(r, 10000000, 1000000000000)
		if asset.RateLimit.Active {
			supply, found := k.GetAssetSupply(ctx, asset.Denom)
			if !found {
				return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("issuance - no asset supply for %s", asset.Denom)
			}
			if asset.RateLimit.Limit.LT(supply.CurrentSupply.Amount.Add(sdk.NewInt(int64(randomAmount)))) {
				maxAmount := asset.RateLimit.Limit.Sub(supply.CurrentSupply.Amount)
				if maxAmount.IsPositive() && maxAmount.GT(sdk.OneInt()) {
					randomAmount = simulation.RandIntBetween(r, 1, int(maxAmount.Int64()))
				} else if maxAmount.Equal(sdk.OneInt()) {
					randomAmount = 1
				} else {
					return simulation.NoOpMsg(types.ModuleName), nil, nil
				}
			}

		}
		msg := types.NewMsgIssueTokens(asset.Owner, sdk.NewCoin(asset.Denom, sdk.NewInt(int64(randomAmount))), recipient.GetAddress())
		spendableCoins := ownerAcc.SpendableCoins(ctx.BlockTime())
		fees, err := simulation.RandomFees(r, ctx, spendableCoins)
		if err != nil {
			return simulation.NoOpMsg(types.ModuleName), nil, err
		}
		tx := helpers.GenTx(
			[]sdk.Msg{msg},
			fees,
			helpers.DefaultGenTxGas,
			chainID,
			[]uint64{ownerAcc.GetAccountNumber()},
			[]uint64{ownerAcc.GetSequence()},
			ownerSimAcc.PrivKey,
		)

		_, _, err = app.Deliver(tx)
		if err != nil {
			fmt.Printf("error on issue %s\n%s\n", msg, asset)
			return simulation.NoOpMsg(types.ModuleName), nil, err
		}
		return simulation.NewOperationMsg(msg, true, ""), nil, nil
	}
}

// SimulateMsgRedeemTokens generates a MsgRedeemTokens with random values
func SimulateMsgRedeemTokens(ak types.AccountKeeper, k keeper.Keeper) simulation.Operation {
	return func(
		r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
	) (simulation.OperationMsg, []simulation.FutureOperation, error) {
		// shuffle the assets and get a random one
		assets := k.GetParams(ctx).Assets
		r.Shuffle(len(assets), func(i, j int) {
			assets[i], assets[j] = assets[j], assets[i]
		})
		asset := assets[0]
		if asset.Paused {
			return simulation.NoOpMsg(types.ModuleName), nil, nil
		}

		// make sure owner account exists
		ownerSimAcc, found := simulation.FindAccount(accs, asset.Owner)
		if !found {
			return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("asset owner not found: %s", asset)
		}

		ownerAcc := ak.GetAccount(ctx, asset.Owner)

		spendableCoinAmount := ownerAcc.SpendableCoins(ctx.BlockTime()).AmountOf(asset.Denom)
		if spendableCoinAmount.IsZero() {
			return simulation.NoOpMsg(types.ModuleName), nil, nil
		}
		var redeemAmount sdk.Int
		if spendableCoinAmount.Equal(sdk.OneInt()) {
			redeemAmount = sdk.OneInt()
		} else {
			redeemAmount = sdk.NewInt(int64(simulation.RandIntBetween(r, 1, int(spendableCoinAmount.Int64()))))
		}

		msg := types.NewMsgRedeemTokens(asset.Owner, sdk.NewCoin(asset.Denom, redeemAmount))
		spendableCoins := ownerAcc.SpendableCoins(ctx.BlockTime()).Sub(sdk.NewCoins(sdk.NewCoin(asset.Denom, redeemAmount)))
		fees, err := simulation.RandomFees(r, ctx, spendableCoins)
		if err != nil {
			fmt.Printf("%s\n", msg)
			return simulation.NoOpMsg(types.ModuleName), nil, err
		}
		tx := helpers.GenTx(
			[]sdk.Msg{msg},
			fees,
			helpers.DefaultGenTxGas,
			chainID,
			[]uint64{ownerAcc.GetAccountNumber()},
			[]uint64{ownerAcc.GetSequence()},
			ownerSimAcc.PrivKey,
		)

		_, _, err = app.Deliver(tx)
		if err != nil {
			fmt.Printf("error on redeem %s\n%s\n", msg, asset)
			return simulation.NoOpMsg(types.ModuleName), nil, err
		}
		return simulation.NewOperationMsg(msg, true, ""), nil, nil
	}
}

// SimulateMsgBlockAddress generates a MsgBlockAddress with random values
func SimulateMsgBlockAddress(ak types.AccountKeeper, k keeper.Keeper) simulation.Operation {
	return func(
		r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
	) (simulation.OperationMsg, []simulation.FutureOperation, error) {
		// shuffle the assets and get a random one
		assets := k.GetParams(ctx).Assets
		r.Shuffle(len(assets), func(i, j int) {
			assets[i], assets[j] = assets[j], assets[i]
		})
		asset := assets[0]

		// make sure owner account exists
		ownerSimAcc, found := simulation.FindAccount(accs, asset.Owner)
		if !found {
			return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("asset owner not found: %s", asset)
		}
		ownerAcc := ak.GetAccount(ctx, asset.Owner)

		// find an account to block
		simAccount, _ := simulation.RandomAcc(r, accs)
		blockedAccount := ak.GetAccount(ctx, simAccount.Address)
		if blockedAccount.GetAddress().Equals(asset.Owner) {
			return simulation.NoOpMsg(types.ModuleName), nil, nil
		}
		for _, blockedAddr := range asset.BlockedAddresses {
			if blockedAccount.GetAddress().Equals(blockedAddr) {
				return simulation.NoOpMsg(types.ModuleName), nil, nil
			}
		}

		msg := types.NewMsgBlockAddress(asset.Owner, asset.Denom, blockedAccount.GetAddress())
		spendableCoins := ownerAcc.SpendableCoins(ctx.BlockTime())
		fees, err := simulation.RandomFees(r, ctx, spendableCoins)
		if err != nil {
			return simulation.NoOpMsg(types.ModuleName), nil, err
		}
		tx := helpers.GenTx(
			[]sdk.Msg{msg},
			fees,
			helpers.DefaultGenTxGas*2,
			chainID,
			[]uint64{ownerAcc.GetAccountNumber()},
			[]uint64{ownerAcc.GetSequence()},
			ownerSimAcc.PrivKey,
		)

		_, _, err = app.Deliver(tx)
		if err != nil {
			fmt.Printf("error on block %s\n%s\n", msg, asset)
			return simulation.NoOpMsg(types.ModuleName), nil, err
		}
		return simulation.NewOperationMsg(msg, true, ""), nil, nil
	}
}

// SimulateMsgPause generates a MsgChangePauseStatus with random values
func SimulateMsgPause(ak types.AccountKeeper, k keeper.Keeper) simulation.Operation {
	return func(
		r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
	) (simulation.OperationMsg, []simulation.FutureOperation, error) {
		// shuffle the assets and get a random one
		assets := k.GetParams(ctx).Assets
		r.Shuffle(len(assets), func(i, j int) {
			assets[i], assets[j] = assets[j], assets[i]
		})
		asset := assets[0]

		// make sure owner account exists
		ownerSimAcc, found := simulation.FindAccount(accs, asset.Owner)
		if !found {
			return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("asset owner not found: %s", asset)
		}
		ownerAcc := ak.GetAccount(ctx, asset.Owner)

		// set status to paused/un-paused
		status := r.Intn(2) == 0

		msg := types.NewMsgSetPauseStatus(asset.Owner, asset.Denom, status)
		spendableCoins := ownerAcc.SpendableCoins(ctx.BlockTime())
		fees, err := simulation.RandomFees(r, ctx, spendableCoins)
		if err != nil {
			return simulation.NoOpMsg(types.ModuleName), nil, err
		}
		tx := helpers.GenTx(
			[]sdk.Msg{msg},
			fees,
			helpers.DefaultGenTxGas*2,
			chainID,
			[]uint64{ownerAcc.GetAccountNumber()},
			[]uint64{ownerAcc.GetSequence()},
			ownerSimAcc.PrivKey,
		)

		_, _, err = app.Deliver(tx)
		if err != nil {
			fmt.Printf("error on pause %s\n%s\n", msg, asset)
			return simulation.NoOpMsg(types.ModuleName), nil, err
		}
		return simulation.NewOperationMsg(msg, true, ""), nil, nil
	}
}