package app

import (
	"fmt"
	"os"

	"github.com/cosmos/cosmos-sdk/codec"
	sdk "github.com/cosmos/cosmos-sdk/types"
	jsoniter "github.com/json-iterator/go"
	"github.com/tendermint/tendermint/libs/log"
	dbm "github.com/tendermint/tm-db"

	"github.com/maticnetwork/heimdall/types/module"
	simTypes "github.com/maticnetwork/heimdall/types/simulation"
)

// SimAppChainID hardcoded chainID for simulation
const (
	SimAppChainID = "simulation-app"
)

// SetupSimulation creates the config, db (levelDB), temporary directory and logger for
// the simulation tests. If `FlagEnabledValue` is false it skips the current test.
// Returns error on an invalid db instantiation or temp dir creation.
func SetupSimulation(dirPrefix, dbName string) (simTypes.Config, dbm.DB, string, log.Logger, bool, error) {
	config := NewConfigFromFlags()
	config.ChainID = SimAppChainID

	var lggr log.Logger
	if FlagVerboseValue {
		lggr = log.TestingLogger()
	} else {
		lggr = log.NewNopLogger()
	}

	dir, err := os.MkdirTemp("", dirPrefix)
	if err != nil {
		return simTypes.Config{}, nil, "", nil, false, err
	}

	db, err := sdk.NewLevelDB(dbName, dir)
	if err != nil {
		return simTypes.Config{}, nil, "", nil, false, err
	}

	return config, db, dir, lggr, false, nil
}

// SimulationOperations retrieves the simulation params from the provided file path
// and returns all the modules weighted operations
func SimulationOperations(app App, cdc *codec.Codec, config simTypes.Config) []simTypes.WeightedOperation {
	simState := module.SimulationState{
		AppParams: make(simTypes.AppParams),
		Cdc:       cdc,
	}

	if config.ParamsFile != "" {
		bz, err := os.ReadFile(config.ParamsFile)
		if err != nil {
			panic(err)
		}

		app.Codec().MustUnmarshalJSON(bz, &simState.AppParams)
	}

	simState.ParamChanges = app.SimulationManager().GenerateParamChanges(config.Seed)
	simState.Contents = app.SimulationManager().GetProposalContents(simState)

	return app.SimulationManager().WeightedOperations(simState)
}

// CheckExportSimulation exports the app state and simulation parameters to JSON
// if the export paths are defined.
func CheckExportSimulation(app App, config simTypes.Config, params simTypes.Params) error {
	if config.ExportStatePath != "" {
		fmt.Println("exporting app state...")

		appState, _, err := app.ExportAppStateAndValidators()
		if err != nil {
			return err
		}

		if err = os.WriteFile(config.ExportStatePath, appState, 0644); err != nil { //nolint
			return err
		}
	}

	if config.ExportParamsPath != "" {
		fmt.Println("exporting simulation params...")

		paramsBz, err := jsoniter.ConfigCompatibleWithStandardLibrary.MarshalIndent(params, "", " ")
		if err != nil {
			return err
		}

		if err = os.WriteFile(config.ExportParamsPath, paramsBz, 0644); err != nil { //nolint
			return err
		}
	}

	return nil
}

// PrintStats prints the corresponding statistics from the app DB.
func PrintStats(db dbm.DB) {
	fmt.Println("\nLevelDB Stats")
	fmt.Println(db.Stats()["leveldb.stats"])
	fmt.Println("LevelDB cached block size", db.Stats()["leveldb.cachedblock"])
}

// GetSimulationLog unmarshals the KVPair's Value to the corresponding type based on the
// each's module store key and the prefix bytes of the KVPair's key.
func GetSimulationLog(storeName string, sdr module.StoreDecoderRegistry, cdc *codec.Codec, kvAs, kvBs []sdk.KVPair) (log string) {
	for i := 0; i < len(kvAs); i++ {
		if len(kvAs[i].Value) == 0 && len(kvBs[i].Value) == 0 {
			// skip if the value doesn't have any bytes
			continue
		}

		decoder, ok := sdr[storeName]
		if ok {
			log += decoder(cdc, kvAs[i], kvBs[i])
		} else {
			log += fmt.Sprintf("store A %X => %X\nstore B %X => %X\n", kvAs[i].Key, kvAs[i].Value, kvBs[i].Key, kvBs[i].Value)
		}
	}

	return
}
