package genesis

import (
	"encoding/json"
	"fmt"
	"time"

	"cosmossdk.io/math"
	"github.com/celestiaorg/celestia-app/v6/app/params"
	blobtypes "github.com/celestiaorg/celestia-app/v6/x/blob/types"
	minfeetypes "github.com/celestiaorg/celestia-app/v6/x/minfee/types"
	"github.com/cosmos/cosmos-sdk/codec"
	sdk "github.com/cosmos/cosmos-sdk/types"
	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
	govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
	govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
	slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
)

// Modifier allows for arbitrary changes to be made on the genesis state
// after initial accounts have been added. It accepts the genesis state as input
// and is expected to return the modified genesis as output.
type Modifier func(state map[string]json.RawMessage) map[string]json.RawMessage

// SetBlobParams will set the provided blob params as genesis state.
func SetBlobParams(codec codec.Codec, params blobtypes.Params) Modifier {
	return func(state map[string]json.RawMessage) map[string]json.RawMessage {
		blobGenState := blobtypes.DefaultGenesis()
		blobGenState.Params = params
		state[blobtypes.ModuleName] = codec.MustMarshalJSON(blobGenState)
		return state
	}
}

// SetMinGasPrice will set the provided minimum gas price as genesis state.
// The minGasPrice parameter should be a float64 representing the gas price.
func SetMinGasPrice(codec codec.Codec, minGasPrice float64) Modifier {
	return func(state map[string]json.RawMessage) map[string]json.RawMessage {
		minFeeGenState := minfeetypes.DefaultGenesis()
		gasPrice := math.LegacyMustNewDecFromStr(fmt.Sprintf("%f", minGasPrice))
		minFeeGenState.Params.NetworkMinGasPrice = gasPrice
		state[minfeetypes.ModuleName] = codec.MustMarshalJSON(minFeeGenState)
		return state
	}
}

// SetSlashingParams will set the provided slashing params as genesis state.
func SetSlashingParams(codec codec.Codec, params slashingtypes.Params) Modifier {
	return func(state map[string]json.RawMessage) map[string]json.RawMessage {
		slashingGenState := slashingtypes.DefaultGenesisState()
		slashingGenState.Params = params
		state[slashingtypes.ModuleName] = codec.MustMarshalJSON(slashingGenState)
		return state
	}
}

// ImmediateProposals sets the thresholds for getting a gov proposal to very low
// levels.
func ImmediateProposals(codec codec.Codec) Modifier {
	return func(state map[string]json.RawMessage) map[string]json.RawMessage {
		gs := govv1.DefaultGenesisState()
		gs.Params.MinDeposit = sdk.NewCoins(sdk.NewCoin(params.BondDenom, math.NewInt(1)))
		gs.Params.Quorum = "0.000001"
		gs.Params.Threshold = "0.000001"
		vp := time.Second * 10 // Extended from 4s to 10s to reduce flakiness
		gs.Params.VotingPeriod = &vp
		state[govtypes.ModuleName] = codec.MustMarshalJSON(gs)
		return state
	}
}

// FundAccounts adds a set of accounts to the genesis and then sets their balance as provided.
// This is good in the case where you have a separate keyring you want to test against and not
// use the one generated by the testnet infra.
func FundAccounts(codec codec.Codec, addresses []sdk.AccAddress, balance sdk.Coin) Modifier {
	return func(state map[string]json.RawMessage) map[string]json.RawMessage {
		// set the accounts in the genesis state
		var authGenState authtypes.GenesisState
		codec.MustUnmarshalJSON(state[authtypes.ModuleName], &authGenState)

		genAccounts := make([]authtypes.GenesisAccount, len(addresses))
		genBalances := make([]banktypes.Balance, len(addresses))
		for idx, addr := range addresses {
			genAccounts[idx] = authtypes.NewBaseAccount(addr, nil, uint64(idx+len(authGenState.Accounts)), 0)
			genBalances[idx] = banktypes.Balance{Address: addr.String(), Coins: sdk.NewCoins(balance)}
		}

		accounts, err := authtypes.PackAccounts(genAccounts)
		if err != nil {
			panic(err)
		}

		authGenState.Accounts = append(authGenState.Accounts, accounts...)
		state[authtypes.ModuleName] = codec.MustMarshalJSON(&authGenState)

		// set the balances in the genesis state
		var bankGenState banktypes.GenesisState
		codec.MustUnmarshalJSON(state[banktypes.ModuleName], &bankGenState)

		bankGenState.Balances = append(bankGenState.Balances, genBalances...)
		state[banktypes.ModuleName] = codec.MustMarshalJSON(&bankGenState)
		return state
	}
}
