package simulation

import (
	"context"
	"encoding/json"
	"math/rand"
	"os"

	wasmvmtypes "wasmvm.mleku.dev/types"

	errorsmod "cosmossdk.io/errors"

	"github.com/cosmos/cosmos-sdk/baseapp"
	"github.com/cosmos/cosmos-sdk/codec"
	codectypes "github.com/cosmos/cosmos-sdk/codec/types"
	sdk "github.com/cosmos/cosmos-sdk/types"
	simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
	"github.com/cosmos/cosmos-sdk/x/auth/tx"
	"github.com/cosmos/cosmos-sdk/x/simulation"

	wasmkeeper "wasmd.mleku.dev/x/wasm/keeper"
	"wasmd.mleku.dev/x/wasm/keeper/testdata"
	"wasmd.mleku.dev/x/wasm/types"
)

// Simulation operation weights constants
const (
	OpWeightMsgStoreCode           = "op_weight_msg_store_code"
	OpWeightMsgInstantiateContract = "op_weight_msg_instantiate_contract"
	OpWeightMsgExecuteContract     = "op_weight_msg_execute_contract"
	OpWeightMsgUpdateAdmin         = "op_weight_msg_update_admin"
	OpWeightMsgClearAdmin          = "op_weight_msg_clear_admin"
	OpWeightMsgMigrateContract     = "op_weight_msg_migrate_contract"
	OpReflectContractPath          = "op_reflect_contract_path"

	DefaultWeightMsgStoreCode           int = 50
	DefaultWeightMsgInstantiateContract int = 100
	DefaultWeightMsgExecuteContract     int = 100
	DefaultWeightMsgUpdateAdmin         int = 25
	DefaultWeightMsgClearAdmin          int = 10
	DefaultWeightMsgMigrateContract     int = 50
)

// WasmKeeper is a subset of the wasm keeper used by simulations
type WasmKeeper interface {
	GetAuthority() string
	GetParams(ctx context.Context) types.Params
	IterateCodeInfos(ctx context.Context, cb func(uint64, types.CodeInfo) bool)
	IterateContractInfo(ctx context.Context, cb func(sdk.AccAddress, types.ContractInfo) bool)
	QuerySmart(ctx context.Context, contractAddr sdk.AccAddress, req []byte) ([]byte, error)
	PeekAutoIncrementID(ctx context.Context, lastIDKey []byte) (uint64, error)
}
type BankKeeper interface {
	simulation.BankKeeper
	IsSendEnabledCoin(ctx context.Context, coin sdk.Coin) bool
}

// WeightedOperations returns all the operations from the module with their respective weights
func WeightedOperations(
	appParams simtypes.AppParams,
	ak types.AccountKeeper,
	bk BankKeeper,
	wasmKeeper WasmKeeper,
) simulation.WeightedOperations {
	var (
		weightMsgStoreCode           int
		weightMsgInstantiateContract int
		weightMsgExecuteContract     int
		weightMsgUpdateAdmin         int
		weightMsgClearAdmin          int
		weightMsgMigrateContract     int
		wasmContractPath             string
	)
	appParams.GetOrGenerate(OpWeightMsgStoreCode, &weightMsgStoreCode, nil, func(_ *rand.Rand) {
		weightMsgStoreCode = DefaultWeightMsgStoreCode
	})
	appParams.GetOrGenerate(OpWeightMsgInstantiateContract, &weightMsgInstantiateContract, nil, func(_ *rand.Rand) {
		weightMsgInstantiateContract = DefaultWeightMsgInstantiateContract
	})
	appParams.GetOrGenerate(OpWeightMsgExecuteContract, &weightMsgInstantiateContract, nil, func(_ *rand.Rand) {
		weightMsgExecuteContract = DefaultWeightMsgExecuteContract
	})
	appParams.GetOrGenerate(OpWeightMsgUpdateAdmin, &weightMsgUpdateAdmin, nil, func(_ *rand.Rand) {
		weightMsgUpdateAdmin = DefaultWeightMsgUpdateAdmin
	})
	appParams.GetOrGenerate(OpWeightMsgClearAdmin, &weightMsgClearAdmin, nil, func(_ *rand.Rand) {
		weightMsgClearAdmin = DefaultWeightMsgClearAdmin
	})
	appParams.GetOrGenerate(OpWeightMsgMigrateContract, &weightMsgMigrateContract, nil, func(_ *rand.Rand) {
		weightMsgMigrateContract = DefaultWeightMsgMigrateContract
	})
	appParams.GetOrGenerate(OpReflectContractPath, &wasmContractPath, nil, func(_ *rand.Rand) {
		wasmContractPath = ""
	})

	var wasmBz []byte
	if wasmContractPath == "" {
		wasmBz = testdata.MigrateReflectContractWasm()
	} else {
		var err error
		wasmBz, err = os.ReadFile(wasmContractPath)
		if err != nil {
			panic(err)
		}
	}
	return simulation.WeightedOperations{
		simulation.NewWeightedOperation(
			weightMsgStoreCode,
			SimulateMsgStoreCode(ak, bk, wasmKeeper, wasmBz),
		),
		simulation.NewWeightedOperation(
			weightMsgInstantiateContract,
			SimulateMsgInstantiateContract(ak, bk, wasmKeeper, DefaultSimulationCodeIDSelector),
		),
		simulation.NewWeightedOperation(
			weightMsgExecuteContract,
			SimulateMsgExecuteContract(
				ak,
				bk,
				wasmKeeper,
				DefaultSimulationExecuteContractSelector,
				DefaultSimulationExecuteSenderSelector,
				DefaultSimulationExecutePayloader,
			),
		),
		simulation.NewWeightedOperation(
			weightMsgUpdateAdmin,
			SimulateMsgUpdateAmin(
				ak,
				bk,
				wasmKeeper,
				DefaultSimulationUpdateAdminContractSelector,
			),
		),
		simulation.NewWeightedOperation(
			weightMsgClearAdmin,
			SimulateMsgClearAdmin(
				ak,
				bk,
				wasmKeeper,
				DefaultSimulationClearAdminContractSelector,
			),
		),
		simulation.NewWeightedOperation(
			weightMsgMigrateContract,
			SimulateMsgMigrateContract(
				ak,
				bk,
				wasmKeeper,
				DefaultSimulationMigrateContractSelector,
				DefaultSimulationMigrateCodeIDSelector,
			),
		),
	}
}

type (
	MsgMigrateContractSelector func(sdk.Context, WasmKeeper, string) (sdk.AccAddress, types.ContractInfo)
	MsgMigrateCodeIDSelector   func(sdk.Context, WasmKeeper, uint64) uint64
)

func DefaultSimulationMigrateContractSelector(ctx sdk.Context, wasmKeeper WasmKeeper, adminAddress string) (sdk.AccAddress,
	types.ContractInfo) {
	var contractAddress sdk.AccAddress
	var contractInfo types.ContractInfo
	wasmKeeper.IterateContractInfo(ctx, func(address sdk.AccAddress, info types.ContractInfo) bool {
		if info.Admin != adminAddress {
			return false
		}
		contractAddress = address
		contractInfo = info
		return true
	})
	return contractAddress, contractInfo
}

func DefaultSimulationMigrateCodeIDSelector(ctx sdk.Context, wasmKeeper WasmKeeper, currentCodeID uint64) uint64 {
	var codeID uint64
	wasmKeeper.IterateCodeInfos(ctx, func(u uint64, info types.CodeInfo) bool {
		if (info.InstantiateConfig.Permission != types.AccessTypeEverybody) || (u == currentCodeID) {
			return false
		}
		codeID = u
		return true
	})
	return codeID
}

func SimulateMsgMigrateContract(
	ak types.AccountKeeper,
	bk BankKeeper,
	wasmKeeper WasmKeeper,
	contractSelector MsgMigrateContractSelector,
	codeIDSelector MsgMigrateCodeIDSelector,
) 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)
		ctAddress, info := contractSelector(ctx, wasmKeeper, simAccount.Address.String())
		if ctAddress == nil {
			return simtypes.NoOpMsg(types.ModuleName, types.MsgMigrateContract{}.Type(),
				"no contract instance available"), nil, nil
		}

		codeID := codeIDSelector(ctx, wasmKeeper, info.CodeID)
		if codeID == 0 {
			return simtypes.NoOpMsg(types.ModuleName, types.MsgMigrateContract{}.Type(),
				"no target contract available"), nil, nil
		}
		migrateMsg := types.MsgMigrateContract{
			Sender:   simAccount.Address.String(),
			Contract: ctAddress.String(),
			CodeID:   codeID,
			Msg:      []byte(`{}`),
		}

		txCtx := BuildOperationInput(r, app, ctx, &migrateMsg, simAccount, ak, bk, nil)
		return simulation.GenAndDeliverTxWithRandFees(txCtx)
	}
}

type MsgClearAdminContractSelector func(sdk.Context, WasmKeeper, string) sdk.AccAddress

func DefaultSimulationClearAdminContractSelector(ctx sdk.Context, wasmKeeper WasmKeeper, adminAddress string) sdk.AccAddress {
	var ctAddress sdk.AccAddress
	wasmKeeper.IterateContractInfo(ctx, func(addr sdk.AccAddress, info types.ContractInfo) bool {
		if info.Admin != adminAddress {
			return false
		}
		ctAddress = addr
		return true
	})
	return ctAddress
}

func SimulateMsgClearAdmin(
	ak types.AccountKeeper,
	bk BankKeeper,
	wasmKeeper WasmKeeper,
	contractSelector MsgClearAdminContractSelector,
) simtypes.Operation {
	return func(
		r *rand.Rand,
		app *baseapp.BaseApp,
		ctx sdk.Context,
		accounts []simtypes.Account,
		chainID string,
	) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) {
		simAccount, _ := simtypes.RandomAcc(r, accounts)
		ctAddress := contractSelector(ctx, wasmKeeper, simAccount.Address.String())
		if ctAddress == nil {
			return simtypes.NoOpMsg(types.ModuleName, types.MsgClearAdmin{}.Type(), "no contract instance available"), nil, nil
		}

		msg := types.MsgClearAdmin{
			Sender:   simAccount.Address.String(),
			Contract: ctAddress.String(),
		}
		txCtx := BuildOperationInput(r, app, ctx, &msg, simAccount, ak, bk, nil)
		return simulation.GenAndDeliverTxWithRandFees(txCtx)
	}
}

type MsgUpdateAdminContractSelector func(sdk.Context, WasmKeeper, string) (sdk.AccAddress, types.ContractInfo)

// DefaultSimulationUpdateAdminContractSelector picks the first contract which Admin != ""
func DefaultSimulationUpdateAdminContractSelector(ctx sdk.Context, wasmKeeper WasmKeeper, adminAddress string) (sdk.AccAddress,
	types.ContractInfo) {
	var contractAddress sdk.AccAddress
	var contractInfo types.ContractInfo
	wasmKeeper.IterateContractInfo(ctx, func(address sdk.AccAddress, info types.ContractInfo) bool {
		if info.Admin != adminAddress {
			return false
		}
		contractAddress = address
		contractInfo = info
		return true
	})
	return contractAddress, contractInfo
}

func SimulateMsgUpdateAmin(
	ak types.AccountKeeper,
	bk BankKeeper,
	wasmKeeper WasmKeeper,
	contractSelector MsgUpdateAdminContractSelector,
) 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)
		ctAddress, _ := contractSelector(ctx, wasmKeeper, simAccount.Address.String())
		if ctAddress == nil {
			return simtypes.NoOpMsg(types.ModuleName, types.MsgUpdateAdmin{}.Type(), "no contract instance available"), nil, nil
		}

		newAdmin, _ := simtypes.RandomAcc(r, accs)
		if newAdmin.Address.String() == simAccount.Address.String() {
			return simtypes.NoOpMsg(types.ModuleName, types.MsgUpdateAdmin{}.Type(),
				"new admin cannot be the same as current admin"), nil, nil
		}

		msg := types.MsgUpdateAdmin{
			Sender:   simAccount.Address.String(),
			NewAdmin: newAdmin.Address.String(),
			Contract: ctAddress.String(),
		}
		txCtx := BuildOperationInput(r, app, ctx, &msg, simAccount, ak, bk, nil)
		return simulation.GenAndDeliverTxWithRandFees(txCtx)
	}
}

// SimulateMsgStoreCode generates a MsgStoreCode with random values
func SimulateMsgStoreCode(
	ak types.AccountKeeper,
	bk BankKeeper,
	wasmKeeper WasmKeeper,
	wasmBz []byte,
) simtypes.Operation {
	return func(
		r *rand.Rand,
		app *baseapp.BaseApp,
		ctx sdk.Context,
		accs []simtypes.Account,
		chainID string,
	) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
		if wasmKeeper.GetParams(ctx).CodeUploadAccess.Permission != types.AccessTypeEverybody {
			return simtypes.NoOpMsg(types.ModuleName, types.MsgStoreCode{}.Type(), "no chain permission"), nil, nil
		}

		simAccount, _ := simtypes.RandomAcc(r, accs)

		permission := wasmKeeper.GetParams(ctx).InstantiateDefaultPermission
		config := permission.With(simAccount.Address)

		msg := types.MsgStoreCode{
			Sender:                simAccount.Address.String(),
			WASMByteCode:          wasmBz,
			InstantiatePermission: &config,
		}
		txCtx := BuildOperationInput(r, app, ctx, &msg, simAccount, ak, bk, nil)
		return simulation.GenAndDeliverTxWithRandFees(txCtx)
	}
}

// CodeIDSelector returns code id to be used in simulations
type CodeIDSelector = func(ctx sdk.Context, wasmKeeper WasmKeeper) uint64

// DefaultSimulationCodeIDSelector picks the first code id
func DefaultSimulationCodeIDSelector(ctx sdk.Context, wasmKeeper WasmKeeper) uint64 {
	var codeID uint64
	wasmKeeper.IterateCodeInfos(ctx, func(u uint64, info types.CodeInfo) bool {
		if info.InstantiateConfig.Permission != types.AccessTypeEverybody {
			return false
		}
		codeID = u
		return true
	})
	return codeID
}

// SimulateMsgInstantiateContract generates a MsgInstantiateContract with random values
func SimulateMsgInstantiateContract(
	ak types.AccountKeeper,
	bk BankKeeper,
	wasmKeeper WasmKeeper,
	codeSelector CodeIDSelector,
) 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)

		codeID := codeSelector(ctx, wasmKeeper)
		if codeID == 0 {
			return simtypes.NoOpMsg(types.ModuleName, types.MsgInstantiateContract{}.Type(),
				"no codes with permission available"), nil, nil
		}
		deposit := sdk.Coins{}
		spendableCoins := bk.SpendableCoins(ctx, simAccount.Address)
		for _, v := range spendableCoins {
			if bk.IsSendEnabledCoin(ctx, v) {
				deposit = deposit.Add(simtypes.RandSubsetCoins(r, sdk.NewCoins(v))...)
			}
		}

		adminAccount, _ := simtypes.RandomAcc(r, accs)

		msg := types.MsgInstantiateContract{
			Sender: simAccount.Address.String(),
			Admin:  adminAccount.Address.String(),
			CodeID: codeID,
			Label:  simtypes.RandStringOfLength(r, 10),
			Msg:    []byte(`{}`),
			Funds:  deposit,
		}
		txCtx := BuildOperationInput(r, app, ctx, &msg, simAccount, ak, bk, deposit)
		return simulation.GenAndDeliverTxWithRandFees(txCtx)
	}
}

// MsgExecuteContractSelector returns contract address to be used in simulations
type MsgExecuteContractSelector = func(ctx sdk.Context, wasmKeeper WasmKeeper) sdk.AccAddress

// MsgExecutePayloader extension point to modify msg with custom payload
type MsgExecutePayloader func(msg *types.MsgExecuteContract) error

// MsgExecuteSenderSelector extension point that returns the sender address
type MsgExecuteSenderSelector func(wasmKeeper WasmKeeper, ctx sdk.Context, contractAddr sdk.AccAddress,
	accs []simtypes.Account) (simtypes.Account, error)

// SimulateMsgExecuteContract create a execute message a reflect contract instance
func SimulateMsgExecuteContract(
	ak types.AccountKeeper,
	bk BankKeeper,
	wasmKeeper WasmKeeper,
	contractSelector MsgExecuteContractSelector,
	senderSelector MsgExecuteSenderSelector,
	payloader MsgExecutePayloader,
) simtypes.Operation {
	return func(
		r *rand.Rand,
		app *baseapp.BaseApp,
		ctx sdk.Context,
		accs []simtypes.Account,
		chainID string,
	) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
		contractAddr := contractSelector(ctx, wasmKeeper)
		if contractAddr == nil {
			return simtypes.NoOpMsg(types.ModuleName, types.MsgExecuteContract{}.Type(),
				"no contract instance available"), nil, nil
		}
		simAccount, err := senderSelector(wasmKeeper, ctx, contractAddr, accs)
		if err != nil {
			return simtypes.NoOpMsg(types.ModuleName, types.MsgExecuteContract{}.Type(), "query contract owner"), nil, err
		}

		deposit := sdk.Coins{}
		spendableCoins := bk.SpendableCoins(ctx, simAccount.Address)
		for _, v := range spendableCoins {
			if bk.IsSendEnabledCoin(ctx, v) {
				deposit = deposit.Add(simtypes.RandSubsetCoins(r, sdk.NewCoins(v))...)
			}
		}
		if deposit.IsZero() {
			return simtypes.NoOpMsg(types.ModuleName, types.MsgExecuteContract{}.Type(), "broke account"), nil, nil
		}
		msg := types.MsgExecuteContract{
			Sender:   simAccount.Address.String(),
			Contract: contractAddr.String(),
			Funds:    deposit,
		}
		if err := payloader(&msg); err != nil {
			return simtypes.NoOpMsg(types.ModuleName, types.MsgExecuteContract{}.Type(), "contract execute payload"), nil, err
		}

		txCtx := BuildOperationInput(r, app, ctx, &msg, simAccount, ak, bk, deposit)
		return simulation.GenAndDeliverTxWithRandFees(txCtx)
	}
}

// BuildOperationInput helper to build object
func BuildOperationInput(
	r *rand.Rand,
	app *baseapp.BaseApp,
	ctx sdk.Context,
	msg interface {
		sdk.Msg
		Type() string
	},
	simAccount simtypes.Account,
	ak types.AccountKeeper,
	bk BankKeeper,
	deposit sdk.Coins,
) simulation.OperationInput {
	interfaceRegistry := codectypes.NewInterfaceRegistry()
	txConfig := tx.NewTxConfig(codec.NewProtoCodec(interfaceRegistry), tx.DefaultSignModes)
	return simulation.OperationInput{
		R:               r,
		App:             app,
		TxGen:           txConfig,
		Cdc:             nil,
		Msg:             msg,
		Context:         ctx,
		SimAccount:      simAccount,
		AccountKeeper:   ak,
		Bankkeeper:      bk,
		ModuleName:      types.ModuleName,
		CoinsSpentInMsg: deposit,
	}
}

// DefaultSimulationExecuteContractSelector picks the first contract address
func DefaultSimulationExecuteContractSelector(ctx sdk.Context, wasmKeeper WasmKeeper) sdk.AccAddress {
	var r sdk.AccAddress
	wasmKeeper.IterateContractInfo(ctx, func(address sdk.AccAddress, info types.ContractInfo) bool {
		r = address
		return true
	})
	return r
}

// DefaultSimulationExecuteSenderSelector queries reflect contract for owner address and selects accounts
func DefaultSimulationExecuteSenderSelector(wasmKeeper WasmKeeper, ctx sdk.Context, contractAddr sdk.AccAddress,
	accs []simtypes.Account) (simtypes.Account, error) {
	var none simtypes.Account
	bz, err := json.Marshal(testdata.ReflectQueryMsg{Owner: &struct{}{}})
	if err != nil {
		return none, errorsmod.Wrap(err, "build smart query")
	}
	got, err := wasmKeeper.QuerySmart(ctx, contractAddr, bz)
	if err != nil {
		return none, errorsmod.Wrap(err, "exec smart query")
	}
	var ownerRes testdata.OwnerResponse
	if err := json.Unmarshal(got, &ownerRes); err != nil || ownerRes.Owner == "" {
		return none, errorsmod.Wrap(err, "parse smart query response")
	}
	ownerAddr, err := sdk.AccAddressFromBech32(ownerRes.Owner)
	if err != nil {
		return none, errorsmod.Wrap(err, "parse contract owner address")
	}
	simAccount, ok := simtypes.FindAccount(accs, ownerAddr)
	if !ok {
		return none, errorsmod.Wrap(err, "unknown contract owner address")
	}
	return simAccount, nil
}

// DefaultSimulationExecutePayloader implements a bank msg to send the
// tokens from contract account back to original sender
func DefaultSimulationExecutePayloader(msg *types.MsgExecuteContract) error {
	reflectSend := testdata.ReflectHandleMsg{
		Reflect: &testdata.ReflectPayload{
			Msgs: []wasmvmtypes.CosmosMsg{{
				Bank: &wasmvmtypes.BankMsg{
					Send: &wasmvmtypes.SendMsg{
						ToAddress: msg.Sender, //
						Amount:    wasmkeeper.ConvertSdkCoinsToWasmCoins(msg.Funds),
					},
				},
			}},
		},
	}
	reflectSendBz, err := json.Marshal(reflectSend)
	if err != nil {
		return err
	}
	msg.Msg = reflectSendBz
	return nil
}
