package lcdtest

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/pkg/errors"
	"github.com/spf13/viper"

	"github.com/cosmos/cosmos-sdk/baseapp"
	"github.com/cosmos/cosmos-sdk/client"
	"github.com/cosmos/cosmos-sdk/client/flags"
	"github.com/cosmos/cosmos-sdk/client/keys"
	"github.com/cosmos/cosmos-sdk/client/lcd"
	"github.com/cosmos/cosmos-sdk/codec"
	crkeys "github.com/cosmos/cosmos-sdk/crypto/keys"
	"github.com/cosmos/cosmos-sdk/server"
	"github.com/cosmos/cosmos-sdk/store"
	"github.com/cosmos/cosmos-sdk/tests"
	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/cosmos/cosmos-sdk/x/auth"
	authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest"
	authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
	"github.com/cosmos/cosmos-sdk/x/crisis"
	distr "github.com/cosmos/cosmos-sdk/x/distribution"
	"github.com/cosmos/cosmos-sdk/x/genutil"
	"github.com/cosmos/cosmos-sdk/x/mint"
	"github.com/cosmos/cosmos-sdk/x/staking"
	"github.com/cosmos/cosmos-sdk/x/supply"

	tmcfg "github.com/tendermint/tendermint/config"
	"github.com/tendermint/tendermint/crypto"
	"github.com/tendermint/tendermint/crypto/ed25519"
	"github.com/tendermint/tendermint/crypto/secp256k1"
	"github.com/tendermint/tendermint/libs/cli"
	"github.com/tendermint/tendermint/libs/log"
	nm "github.com/tendermint/tendermint/node"
	"github.com/tendermint/tendermint/p2p"
	pvm "github.com/tendermint/tendermint/privval"
	"github.com/tendermint/tendermint/proxy"
	ctypes "github.com/tendermint/tendermint/rpc/core/types"
	tmrpc "github.com/tendermint/tendermint/rpc/lib/server"
	tmtypes "github.com/tendermint/tendermint/types"
	dbm "github.com/tendermint/tm-db"

	"github.com/CosmWasm/wasmd/app"
)

// TODO: Make InitializeTestLCD safe to call in multiple tests at the same time

// InitializeLCD starts Tendermint and the LCD in process, listening on
// their respective sockets where nValidators is the total number of validators
// and initAddrs are the accounts to initialize with some stake tokens. It
// returns a cleanup function, a set of validator public keys, and a port.
func InitializeLCD(nValidators int, initAddrs []sdk.AccAddress, minting bool, portExt ...string) (
	cleanup func(), valConsPubKeys []crypto.PubKey, valOperAddrs []sdk.ValAddress, port string, err error) {

	config, err := GetConfig()
	if err != nil {
		return
	}
	config.Consensus.TimeoutCommit = 100
	config.Consensus.SkipTimeoutCommit = false
	config.TxIndex.IndexAllKeys = true

	logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
	logger = log.NewFilter(logger, log.AllowError())

	db := dbm.NewMemDB()
	gapp := app.NewWasmApp(logger, db, nil, true, 0, nil, baseapp.SetPruning(store.PruneNothing))
	cdc = app.MakeCodec()

	genDoc, valConsPubKeys, valOperAddrs, privVal, err := defaultGenesis(config, nValidators, initAddrs, minting)
	if err != nil {
		return
	}

	var listenAddr string

	if len(portExt) == 0 {
		listenAddr, port, err = server.FreeTCPAddr()
		if err != nil {
			return
		}
	} else {
		listenAddr = fmt.Sprintf("tcp://0.0.0.0:%s", portExt[0])
		port = portExt[0]
	}

	// XXX: Need to set this so LCD knows the tendermint node address!
	viper.Set(flags.FlagNode, config.RPC.ListenAddress)
	viper.Set(flags.FlagChainID, genDoc.ChainID)
	// TODO Set to false once the upstream Tendermint proof verification issue is fixed.
	viper.Set(flags.FlagTrustNode, true)

	node, err := startTM(config, logger, genDoc, privVal, gapp)
	if err != nil {
		return
	}

	tests.WaitForNextHeightTM(tests.ExtractPortFromAddress(config.RPC.ListenAddress))

	lcdInstance, err := startLCD(logger, listenAddr, cdc)
	if err != nil {
		return
	}

	tests.WaitForLCDStart(port)
	tests.WaitForHeight(1, port)

	cleanup = func() {
		logger.Debug("cleaning up LCD initialization")
		err = node.Stop()
		if err != nil {
			logger.Error(err.Error())
		}

		node.Wait()
		err = lcdInstance.Close()
		if err != nil {
			logger.Error(err.Error())
		}
	}

	return cleanup, valConsPubKeys, valOperAddrs, port, err
}

func defaultGenesis(config *tmcfg.Config, nValidators int, initAddrs []sdk.AccAddress, minting bool) (
	genDoc *tmtypes.GenesisDoc, valConsPubKeys []crypto.PubKey, valOperAddrs []sdk.ValAddress, privVal *pvm.FilePV, err error) {
	privVal = pvm.LoadOrGenFilePV(config.PrivValidatorKeyFile(),
		config.PrivValidatorStateFile())
	privVal.Reset()

	if nValidators < 1 {
		err = errors.New("initializeLCD must use at least one validator")
		return
	}

	genesisFile := config.GenesisFile()
	genDoc, err = tmtypes.GenesisDocFromFile(genesisFile)
	if err != nil {
		return
	}
	genDoc.Validators = nil
	err = genDoc.SaveAs(genesisFile)
	if err != nil {
		return
	}

	// append any additional (non-proposing) validators
	//nolint:prealloc
	var (
		genTxs      []auth.StdTx
		genAccounts []authexported.GenesisAccount
	)
	totalSupply := sdk.ZeroInt()

	for i := 0; i < nValidators; i++ {
		operPrivKey := secp256k1.GenPrivKey()
		operAddr := operPrivKey.PubKey().Address()
		pubKey := privVal.GetPubKey()

		power := int64(100)
		if i > 0 {
			pubKey = ed25519.GenPrivKey().PubKey()
			power = 1
		}

		startTokens := sdk.TokensFromConsensusPower(power)

		msg := staking.NewMsgCreateValidator(
			sdk.ValAddress(operAddr),
			pubKey,
			sdk.NewCoin(sdk.DefaultBondDenom, startTokens),
			staking.NewDescription(fmt.Sprintf("validator-%d", i+1), "", "", "", ""),
			staking.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()),
			sdk.OneInt(),
		)

		stdSignMsg := auth.StdSignMsg{
			ChainID: genDoc.ChainID,
			Msgs:    []sdk.Msg{msg},
		}

		var sig []byte
		sig, err = operPrivKey.Sign(stdSignMsg.Bytes())
		if err != nil {
			return
		}

		transaction := auth.NewStdTx([]sdk.Msg{msg}, auth.StdFee{}, []auth.StdSignature{{Signature: sig, PubKey: operPrivKey.PubKey()}}, "")
		genTxs = append(genTxs, transaction)
		valConsPubKeys = append(valConsPubKeys, pubKey)
		valOperAddrs = append(valOperAddrs, sdk.ValAddress(operAddr))

		account := auth.NewBaseAccountWithAddress(sdk.AccAddress(operAddr))
		accTokens := sdk.TokensFromConsensusPower(150)
		totalSupply = totalSupply.Add(accTokens)

		account.Coins = sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, accTokens))
		genAccounts = append(genAccounts, &account)
	}

	genesisState := app.NewDefaultGenesisState()
	genDoc.AppState, err = cdc.MarshalJSON(genesisState)
	if err != nil {
		return
	}

	genesisState, err = genutil.SetGenTxsInAppGenesisState(cdc, genesisState, genTxs)
	if err != nil {
		return
	}

	// add some tokens to init accounts
	for _, addr := range initAddrs {
		accAuth := auth.NewBaseAccountWithAddress(addr)
		accTokens := sdk.TokensFromConsensusPower(100)
		accAuth.Coins = sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, accTokens)}
		totalSupply = totalSupply.Add(accTokens)

		genAccounts = append(genAccounts, &accAuth)
	}

	// auth genesis state: params and genesis accounts
	authDataBz := genesisState[auth.ModuleName]
	var authGenState auth.GenesisState
	cdc.MustUnmarshalJSON(authDataBz, &authGenState)
	authGenState.Accounts = genAccounts
	genesisState[auth.ModuleName] = cdc.MustMarshalJSON(authGenState)

	stakingDataBz := genesisState[staking.ModuleName]
	var stakingData staking.GenesisState
	cdc.MustUnmarshalJSON(stakingDataBz, &stakingData)
	genesisState[staking.ModuleName] = cdc.MustMarshalJSON(stakingData)

	// distr data
	distrDataBz := genesisState[distr.ModuleName]
	var distrData distr.GenesisState
	cdc.MustUnmarshalJSON(distrDataBz, &distrData)

	commPoolAmt := sdk.NewInt(10)
	distrData.FeePool.CommunityPool = sdk.DecCoins{sdk.NewDecCoin(sdk.DefaultBondDenom, commPoolAmt)}
	distrDataBz = cdc.MustMarshalJSON(distrData)
	genesisState[distr.ModuleName] = distrDataBz

	// supply data
	supplyDataBz := genesisState[supply.ModuleName]
	var supplyData supply.GenesisState
	cdc.MustUnmarshalJSON(supplyDataBz, &supplyData)

	supplyData.Supply = sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, totalSupply.Add(commPoolAmt)))
	supplyDataBz = cdc.MustMarshalJSON(supplyData)
	genesisState[supply.ModuleName] = supplyDataBz

	// mint genesis (none set within genesisState)
	mintData := mint.DefaultGenesisState()
	inflationMin := sdk.ZeroDec()
	if minting {
		inflationMin = sdk.MustNewDecFromStr("0.9")
		mintData.Params.InflationMax = sdk.MustNewDecFromStr("1.0")
	} else {
		mintData.Params.InflationMax = inflationMin
	}
	mintData.Minter.Inflation = inflationMin
	mintData.Params.InflationMin = inflationMin
	mintDataBz := cdc.MustMarshalJSON(mintData)
	genesisState[mint.ModuleName] = mintDataBz

	// initialize crisis data
	crisisDataBz := genesisState[crisis.ModuleName]
	var crisisData crisis.GenesisState
	cdc.MustUnmarshalJSON(crisisDataBz, &crisisData)
	crisisData.ConstantFee = sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000)
	crisisDataBz = cdc.MustMarshalJSON(crisisData)
	genesisState[crisis.ModuleName] = crisisDataBz

	//// double check inflation is set according to the minting boolean flag
	if minting {
		if !(mintData.Params.InflationMax.Equal(sdk.MustNewDecFromStr("1.0")) &&
			mintData.Minter.Inflation.Equal(sdk.MustNewDecFromStr("0.9")) &&
			mintData.Params.InflationMin.Equal(sdk.MustNewDecFromStr("0.9"))) {
			err = errors.New("mint parameters does not correspond to their defaults")
			return
		}
	} else {
		if !(mintData.Params.InflationMax.Equal(sdk.ZeroDec()) &&
			mintData.Minter.Inflation.Equal(sdk.ZeroDec()) &&
			mintData.Params.InflationMin.Equal(sdk.ZeroDec())) {
			err = errors.New("mint parameters not equal to decimal 0")
			return
		}
	}

	appState, err := codec.MarshalJSONIndent(cdc, genesisState)
	if err != nil {
		return
	}

	genDoc.AppState = appState
	return genDoc, valConsPubKeys, valOperAddrs, privVal, err
}

// startTM creates and starts an in-process Tendermint node with memDB and
// in-process ABCI application. It returns the new node or any error that
// occurred.
//
// TODO: Clean up the WAL dir or enable it to be not persistent!
func startTM(
	tmcfg *tmcfg.Config, logger log.Logger, genDoc *tmtypes.GenesisDoc,
	privVal tmtypes.PrivValidator, app *app.WasmApp,
) (*nm.Node, error) {

	genDocProvider := func() (*tmtypes.GenesisDoc, error) { return genDoc, nil }
	dbProvider := func(*nm.DBContext) (dbm.DB, error) { return dbm.NewMemDB(), nil }
	nodeKey, err := p2p.LoadOrGenNodeKey(tmcfg.NodeKeyFile())
	if err != nil {
		return nil, err
	}
	node, err := nm.NewNode(
		tmcfg,
		privVal,
		nodeKey,
		proxy.NewLocalClientCreator(app),
		genDocProvider,
		dbProvider,
		nm.DefaultMetricsProvider(tmcfg.Instrumentation),
		logger.With("module", "node"),
	)
	if err != nil {
		return nil, err
	}

	err = node.Start()
	if err != nil {
		return nil, err
	}

	tests.WaitForRPC(tmcfg.RPC.ListenAddress)
	logger.Info("Tendermint running!")

	return node, err
}

// startLCD starts the LCD.
func startLCD(logger log.Logger, listenAddr string, cdc *codec.Codec) (net.Listener, error) {
	rs := lcd.NewRestServer(cdc)
	registerRoutes(rs)
	listener, err := tmrpc.Listen(listenAddr, tmrpc.DefaultConfig())
	if err != nil {
		return nil, err
	}
	go tmrpc.StartHTTPServer(listener, rs.Mux, logger, tmrpc.DefaultConfig()) //nolint:errcheck
	return listener, nil
}

// NOTE: If making updates here also update cmd/gaia/cmd/wasmcli/main.go
func registerRoutes(rs *lcd.RestServer) {
	client.RegisterRoutes(rs.CliCtx, rs.Mux)
	authrest.RegisterTxRoutes(rs.CliCtx, rs.Mux)
	app.ModuleBasics.RegisterRESTRoutes(rs.CliCtx, rs.Mux)
}

var cdc = codec.New()

func init() {
	ctypes.RegisterAmino(cdc)
}

// CreateAddr adds an address to the key store and returns an address and seed.
// It also requires that the key could be created.
func CreateAddr(name string, kb crkeys.Keybase) (sdk.AccAddress, string, error) {
	var (
		err  error
		info crkeys.Info
		seed string
	)
	info, seed, err = kb.CreateMnemonic(name, crkeys.English, keys.DefaultKeyPass, crkeys.Secp256k1)
	if err != nil {
		return nil, "", err
	}
	return sdk.AccAddress(info.GetPubKey().Address()), seed, err
}

// CreateAddrs adds multiple address to the key store and returns the addresses and associated seeds in lexographical order by address.
// It also requires that the keys could be created.
func CreateAddrs(kb crkeys.Keybase, numAddrs int) (addrs []sdk.AccAddress, seeds, names []string, errs []error) {
	var (
		err  error
		info crkeys.Info
		seed string
	)

	addrSeeds := AddrSeedSlice{}

	for i := 0; i < numAddrs; i++ {
		name := fmt.Sprintf("test%d", i)
		info, seed, err = kb.CreateMnemonic(name, crkeys.English, keys.DefaultKeyPass, crkeys.Secp256k1)
		if err != nil {
			errs = append(errs, err)
		}
		addrSeeds = append(addrSeeds, AddrSeed{Address: sdk.AccAddress(info.GetPubKey().Address()), Seed: seed, Name: name})
	}
	if len(errs) > 0 {
		return
	}

	sort.Sort(addrSeeds)

	for i := range addrSeeds {
		addrs = append(addrs, addrSeeds[i].Address)
		seeds = append(seeds, addrSeeds[i].Seed)
		names = append(names, addrSeeds[i].Name)
	}

	return addrs, seeds, names, errs
}

// AddrSeed combines an Address with the mnemonic of the private key to that address
type AddrSeed struct {
	Address sdk.AccAddress
	Seed    string
	Name    string
}

// AddrSeedSlice implements `Interface` in sort package.
type AddrSeedSlice []AddrSeed

func (b AddrSeedSlice) Len() int {
	return len(b)
}

// Less sorts lexicographically by Address
func (b AddrSeedSlice) Less(i, j int) bool {
	// bytes package already implements Comparable for []byte.
	switch bytes.Compare(b[i].Address.Bytes(), b[j].Address.Bytes()) {
	case -1:
		return true
	case 0, 1:
		return false
	default:
		panic("not fail-able with `bytes.Comparable` bounded [-1, 1].")
	}
}

func (b AddrSeedSlice) Swap(i, j int) {
	b[j], b[i] = b[i], b[j]
}

// InitClientHome initialises client home dir.
func InitClientHome(dir string) string {
	var err error
	if dir == "" {
		dir, err = ioutil.TempDir("", "lcd_test")
		if err != nil {
			panic(err)
		}
	}
	// TODO: this should be set in NewRestServer
	// and pass down the CLIContext to achieve
	// parallelism.
	viper.Set(cli.HomeFlag, dir)
	return dir
}

// makePathname creates a unique pathname for each test.
func makePathname() (string, error) {
	p, err := os.Getwd()
	if err != nil {
		return "", err
	}

	sep := string(filepath.Separator)
	return strings.Replace(p, sep, "_", -1), nil
}

// GetConfig returns a Tendermint config for the test cases.
func GetConfig() (*tmcfg.Config, error) {
	pathname, err := makePathname()
	if err != nil {
		return nil, err
	}
	config := tmcfg.ResetTestRoot(pathname)

	tmAddr, _, err := server.FreeTCPAddr()
	if err != nil {
		return nil, err
	}

	rcpAddr, _, err := server.FreeTCPAddr()
	if err != nil {
		return nil, err
	}

	grpcAddr, _, err := server.FreeTCPAddr()
	if err != nil {
		return nil, err
	}

	config.P2P.ListenAddress = tmAddr
	config.RPC.ListenAddress = rcpAddr
	config.RPC.GRPCListenAddress = grpcAddr

	return config, nil
}
