package simulation

import (
	"context"
	"time"

	"git.cw.tr/mukan-network/mukan-sdk/testutil/simsx"
	sdk "git.cw.tr/mukan-network/mukan-sdk/types"
	"git.cw.tr/mukan-network/mukan-sdk/x/authz"
	"git.cw.tr/mukan-network/mukan-sdk/x/authz/keeper"
	banktype "git.cw.tr/mukan-network/mukan-sdk/x/bank/types"
)

func MsgGrantFactory() simsx.SimMsgFactoryFn[*authz.MsgGrant] {
	return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *authz.MsgGrant) {
		granter := testData.AnyAccount(reporter, simsx.WithSpendableBalance())
		grantee := testData.AnyAccount(reporter, simsx.ExcludeAccounts(granter))
		spendLimit := granter.LiquidBalance().RandSubsetCoins(reporter, simsx.WithSendEnabledCoins())

		r := testData.Rand()
		var expiration *time.Time
		if t1 := r.Timestamp(); !t1.Before(simsx.BlockTime(ctx)) {
			expiration = &t1
		}
		// pick random authorization
		authorizations := []authz.Authorization{
			banktype.NewSendAuthorization(spendLimit, nil),
			authz.NewGenericAuthorization(sdk.MsgTypeURL(&banktype.MsgSend{})),
		}
		randomAuthz := simsx.OneOf(r, authorizations)

		msg, err := authz.NewMsgGrant(granter.Address, grantee.Address, randomAuthz, expiration)
		if err != nil {
			reporter.Skip(err.Error())
			return nil, nil
		}
		return []simsx.SimAccount{granter}, msg
	}
}

func MsgExecFactory(k keeper.Keeper) simsx.SimMsgFactoryFn[*authz.MsgExec] {
	return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *authz.MsgExec) {
		bankSendOnlyFilter := func(a authz.Authorization) bool {
			_, ok := a.(*banktype.SendAuthorization)
			return ok
		}
		granterAddr, granteeAddr, gAuthz := findGrant(ctx, k, reporter, bankSendOnlyFilter)
		granter := testData.GetAccountbyAccAddr(reporter, granterAddr)
		grantee := testData.GetAccountbyAccAddr(reporter, granteeAddr)
		if reporter.IsSkipped() {
			return nil, nil
		}
		amount := granter.LiquidBalance().RandSubsetCoins(reporter, simsx.WithSendEnabledCoins())
		amount = amount.Min(gAuthz.(*banktype.SendAuthorization).SpendLimit)
		if !amount.IsAllPositive() {
			reporter.Skip("amount is not positive")
			return nil, nil
		}
		payloadMsg := []sdk.Msg{banktype.NewMsgSend(granter.Address, grantee.Address, amount)}
		msgExec := authz.NewMsgExec(grantee.Address, payloadMsg)
		return []simsx.SimAccount{grantee}, &msgExec
	}
}

func MsgRevokeFactory(k keeper.Keeper) simsx.SimMsgFactoryFn[*authz.MsgRevoke] {
	return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *authz.MsgRevoke) {
		granterAddr, granteeAddr, auth := findGrant(ctx, k, reporter)
		granter := testData.GetAccountbyAccAddr(reporter, granterAddr)
		grantee := testData.GetAccountbyAccAddr(reporter, granteeAddr)
		if reporter.IsSkipped() {
			return nil, nil
		}
		msgExec := authz.NewMsgRevoke(granter.Address, grantee.Address, auth.MsgTypeURL())
		return []simsx.SimAccount{granter}, &msgExec
	}
}

func findGrant(
	ctx context.Context,
	k keeper.Keeper,
	reporter simsx.SimulationReporter,
	acceptFilter ...func(a authz.Authorization) bool,
) (granterAddr, granteeAddr sdk.AccAddress, auth authz.Authorization) {
	var innerErr error
	k.IterateGrants(ctx, func(granter, grantee sdk.AccAddress, grant authz.Grant) bool {
		a, err2 := grant.GetAuthorization()
		if err2 != nil {
			innerErr = err2
			return true
		}
		for _, filter := range acceptFilter {
			if !filter(a) {
				return false
			}
		}
		granterAddr, granteeAddr, auth = granter, grantee, a
		return true
	})
	if innerErr != nil {
		reporter.Skip(innerErr.Error())
		return granterAddr, granteeAddr, auth
	}
	if auth == nil {
		reporter.Skip("no grant found")
	}
	return granterAddr, granteeAddr, auth
}
