package chaincmd

import (
	"fmt"

	"github.com/cosmos/cosmos-sdk/client/flags"
	sdk "github.com/cosmos/cosmos-sdk/types"

	"github.com/ignite/cli/v29/ignite/pkg/cmdrunner/step"
	"github.com/ignite/cli/v29/ignite/pkg/cosmosver"
	"github.com/ignite/cli/v29/ignite/pkg/errors"
)

const (
	commandStart             = "start"
	commandInit              = "init"
	commandKeys              = "keys"
	commandGenesis           = "genesis"
	commandAddGenesisAccount = "add-genesis-account"
	commandGentx             = "gentx"
	commandCollectGentxs     = "collect-gentxs"
	commandValidateGenesis   = "validate"
	commandExportGenssis     = "export"
	commandShowNodeID        = "show-node-id"
	commandStatus            = "status"
	commandTx                = "tx"
	commandQuery             = "query"
	commandUnsafeReset       = "unsafe-reset-all"
	commandTendermint        = "tendermint"
	commandTestnetInPlace    = "in-place-testnet"
	commandTestnetMultiNode  = "multi-node"

	optionHome                             = "--home"
	optionNode                             = "--node"
	optionKeyringBackend                   = "--keyring-backend"
	optionChainID                          = "--chain-id"
	optionOutput                           = "--output"
	optionRecover                          = "--recover"
	optionAddress                          = "--address"
	optionAmount                           = "--amount"
	optionFees                             = "--fees"
	optionValidatorMoniker                 = "--moniker"
	optionValidatorCommissionRate          = "--commission-rate"
	optionValidatorCommissionMaxRate       = "--commission-max-rate"
	optionValidatorCommissionMaxChangeRate = "--commission-max-change-rate"
	optionValidatorMinSelfDelegation       = "--min-self-delegation"
	optionValidatorGasPrices               = "--gas-prices"
	optionValidatorDetails                 = "--details"
	optionValidatorIdentity                = "--identity"
	optionValidatorWebsite                 = "--website"
	optionValidatorSecurityContact         = "--security-contact"
	optionYes                              = "--yes"
	optionHomeClient                       = "--home-client"
	optionCoinType                         = "--coin-type"
	optionVestingAmount                    = "--vesting-amount"
	optionVestingEndTime                   = "--vesting-end-time"
	optionBroadcastMode                    = "--broadcast-mode"
	optionAccount                          = "--account"
	optionIndex                            = "--index"
	optionValidatorPrivateKey              = "--validator-privkey"
	optionAccountToFund                    = "--accounts-to-fund"
	optionSkipConfirmation                 = "--skip-confirmation"
	optionAmountStakes                     = "--validators-stake-amount"
	optionOutPutDir                        = "--output-dir"
	optionNumValidator                     = "--v"
	optionNodeDirPrefix                    = "--node-dir-prefix"
	optionPorts                            = "--list-ports"

	constTendermint = "tendermint"
	constJSON       = "json"
)

type KeyringBackend string

const (
	KeyringBackendUnspecified KeyringBackend = ""
	KeyringBackendOS          KeyringBackend = "os"
	KeyringBackendFile        KeyringBackend = "file"
	KeyringBackendPass        KeyringBackend = "pass"
	KeyringBackendTest        KeyringBackend = "test"
	KeyringBackendKwallet     KeyringBackend = "kwallet"
)

type ChainCmd struct {
	appCmd          string
	chainID         string
	homeDir         string
	keyringBackend  KeyringBackend
	keyringPassword string
	nodeAddress     string

	isAutoChainIDDetectionEnabled bool

	sdkVersion cosmosver.Version
}

// New creates a new ChainCmd to launch command with the chain app.
func New(appCmd string, options ...Option) ChainCmd {
	chainCmd := ChainCmd{
		appCmd:     appCmd,
		sdkVersion: cosmosver.Latest,
	}

	applyOptions(&chainCmd, options)

	return chainCmd
}

// Copy makes a copy of ChainCmd by overwriting its options with given options.
func (c ChainCmd) Copy(options ...Option) ChainCmd {
	applyOptions(&c, options)

	return c
}

// Option configures ChainCmd.
type Option func(*ChainCmd)

func applyOptions(c *ChainCmd, options []Option) {
	for _, apply := range options {
		apply(c)
	}
}

// WithVersion sets the version of the blockchain.
// when this is not provided, the latest version of SDK is assumed.
func WithVersion(v cosmosver.Version) Option {
	return func(c *ChainCmd) {
		c.sdkVersion = v
	}
}

// WithHome replaces the default home used by the chain.
func WithHome(home string) Option {
	return func(c *ChainCmd) {
		c.homeDir = home
	}
}

// WithChainID provides a specific chain ID for the commands that accept this option.
func WithChainID(chainID string) Option {
	return func(c *ChainCmd) {
		c.chainID = chainID
	}
}

// WithAutoChainIDDetection finds out the chain id by communicating with the node running.
func WithAutoChainIDDetection() Option {
	return func(c *ChainCmd) {
		c.isAutoChainIDDetectionEnabled = true
	}
}

// WithKeyringBackend provides a specific keyring backend for the commands that accept this option.
func WithKeyringBackend(keyringBackend KeyringBackend) Option {
	return func(c *ChainCmd) {
		c.keyringBackend = keyringBackend
	}
}

// WithKeyringPassword provides a password to unlock keyring.
func WithKeyringPassword(password string) Option {
	return func(c *ChainCmd) {
		c.keyringPassword = password
	}
}

// WithNodeAddress sets the node address for the commands that needs to make an
// API request to the node that has a different node address other than the default one.
func WithNodeAddress(addr string) Option {
	return func(c *ChainCmd) {
		c.nodeAddress = addr
	}
}

// Name returns the app name (prefix of the chain daemon).
func (c ChainCmd) Name() string {
	return c.appCmd
}

// StartCommand returns the command to start the daemon of the chain.
func (c ChainCmd) StartCommand(options ...string) step.Option {
	command := append([]string{
		commandStart,
	}, options...)
	return c.daemonCommand(command)
}

// InitCommand returns the command to initialize the chain.
func (c ChainCmd) InitCommand(moniker string, options ...string) step.Option {
	command := append([]string{
		commandInit,
		moniker,
	}, options...)
	command = c.attachChainID(command)
	return c.daemonCommand(command)
}

// AddKeyCommand returns the command to add a new key in the chain keyring.
func (c ChainCmd) AddKeyCommand(accountName, coinType, accountNumber, addressIndex string) step.Option {
	command := []string{
		commandKeys,
		"add",
		accountName,
		optionOutput,
		constJSON,
	}
	if coinType != "" {
		command = append(command, optionCoinType, coinType)
	}
	if accountNumber != "" {
		command = append(command, optionAccount, accountNumber)
	}
	if addressIndex != "" {
		command = append(command, optionIndex, addressIndex)
	}
	command = c.attachKeyringBackend(command)

	return c.cliCommand(command)
}

// RecoverKeyCommand returns the command to recover a key into the chain keyring from a mnemonic.
func (c ChainCmd) RecoverKeyCommand(accountName, coinType, accountNumber, addressIndex string) step.Option {
	command := []string{
		commandKeys,
		"add",
		accountName,
		optionRecover,
	}
	if coinType != "" {
		command = append(command, optionCoinType, coinType)
	}
	if accountNumber != "" {
		command = append(command, optionAccount, accountNumber)
	}
	if addressIndex != "" {
		command = append(command, optionIndex, addressIndex)
	}
	command = c.attachKeyringBackend(command)

	return c.cliCommand(command)
}

// ImportKeyCommand returns the command to import a key into the chain keyring from a key file.
func (c ChainCmd) ImportKeyCommand(accountName, keyFile string) step.Option {
	command := []string{
		commandKeys,
		"import",
		accountName,
		keyFile,
	}
	command = c.attachKeyringBackend(command)

	return c.cliCommand(command)
}

// ShowKeyAddressCommand returns the command to print the address of a key in the chain keyring.
func (c ChainCmd) ShowKeyAddressCommand(accountName string) step.Option {
	command := []string{
		commandKeys,
		"show",
		accountName,
		optionAddress,
	}
	command = c.attachKeyringBackend(command)

	return c.cliCommand(command)
}

// ListKeysCommand returns the command to print the list of a keys in the chain keyring.
func (c ChainCmd) ListKeysCommand() step.Option {
	command := []string{
		commandKeys,
		"list",
		optionOutput,
		constJSON,
	}
	command = c.attachKeyringBackend(command)

	return c.cliCommand(command)
}

// AddGenesisAccountCommand returns the command to add a new account in the genesis file of the chain.
func (c ChainCmd) AddGenesisAccountCommand(address, coins string) step.Option {
	command := []string{
		commandGenesis,
		commandAddGenesisAccount,
		address,
		coins,
	}

	return c.daemonCommand(command)
}

// AddVestingAccountCommand returns the command to add a delayed vesting account in the genesis file of the chain.
func (c ChainCmd) AddVestingAccountCommand(address, originalCoins, vestingCoins string, vestingEndTime int64) step.Option {
	command := []string{
		commandGenesis,
		commandAddGenesisAccount,
		address,
		originalCoins,
		optionVestingAmount,
		vestingCoins,
		optionVestingEndTime,
		fmt.Sprintf("%d", vestingEndTime),
	}

	return c.daemonCommand(command)
}

// GentxOption for the GentxCommand.
type GentxOption func([]string) []string

// GentxWithMoniker provides moniker option for the gentx command.
func GentxWithMoniker(moniker string) GentxOption {
	return func(command []string) []string {
		if len(moniker) > 0 {
			return append(command, optionValidatorMoniker, moniker)
		}
		return command
	}
}

// GentxWithCommissionRate provides commission rate option for the gentx command.
func GentxWithCommissionRate(commissionRate string) GentxOption {
	return func(command []string) []string {
		if len(commissionRate) > 0 {
			return append(command, optionValidatorCommissionRate, commissionRate)
		}
		return command
	}
}

// GentxWithCommissionMaxRate provides commission max rate option for the gentx command.
func GentxWithCommissionMaxRate(commissionMaxRate string) GentxOption {
	return func(command []string) []string {
		if len(commissionMaxRate) > 0 {
			return append(command, optionValidatorCommissionMaxRate, commissionMaxRate)
		}
		return command
	}
}

// GentxWithCommissionMaxChangeRate provides commission max change rate option for the gentx command.
func GentxWithCommissionMaxChangeRate(commissionMaxChangeRate string) GentxOption {
	return func(command []string) []string {
		if len(commissionMaxChangeRate) > 0 {
			return append(command, optionValidatorCommissionMaxChangeRate, commissionMaxChangeRate)
		}
		return command
	}
}

// GentxWithMinSelfDelegation provides minimum self delegation option for the gentx command.
func GentxWithMinSelfDelegation(minSelfDelegation string) GentxOption {
	return func(command []string) []string {
		if len(minSelfDelegation) > 0 {
			return append(command, optionValidatorMinSelfDelegation, minSelfDelegation)
		}
		return command
	}
}

// GentxWithGasPrices provides gas price option for the gentx command.
func GentxWithGasPrices(gasPrices string) GentxOption {
	return func(command []string) []string {
		if len(gasPrices) > 0 {
			return append(command, optionValidatorGasPrices, gasPrices)
		}
		return command
	}
}

// GentxWithDetails provides validator details option for the gentx command.
func GentxWithDetails(details string) GentxOption {
	return func(command []string) []string {
		if len(details) > 0 {
			return append(command, optionValidatorDetails, details)
		}
		return command
	}
}

// GentxWithIdentity provides validator identity option for the gentx command.
func GentxWithIdentity(identity string) GentxOption {
	return func(command []string) []string {
		if len(identity) > 0 {
			return append(command, optionValidatorIdentity, identity)
		}
		return command
	}
}

// GentxWithWebsite provides validator website option for the gentx command.
func GentxWithWebsite(website string) GentxOption {
	return func(command []string) []string {
		if len(website) > 0 {
			return append(command, optionValidatorWebsite, website)
		}
		return command
	}
}

// GentxWithSecurityContact provides validator security contact option for the gentx command.
func GentxWithSecurityContact(securityContact string) GentxOption {
	return func(command []string) []string {
		if len(securityContact) > 0 {
			return append(command, optionValidatorSecurityContact, securityContact)
		}
		return command
	}
}

func (c ChainCmd) IsAutoChainIDDetectionEnabled() bool {
	return c.isAutoChainIDDetectionEnabled
}

func (c ChainCmd) SDKVersion() cosmosver.Version {
	return c.sdkVersion
}

// GentxCommand returns the command to generate a gentx for the chain.
func (c ChainCmd) GentxCommand(
	validatorName string,
	selfDelegation string,
	options ...GentxOption,
) step.Option {
	command := []string{
		commandGenesis,
		commandGentx,
	}

	switch {
	case c.sdkVersion.LT(cosmosver.StargateFortyVersion):
		command = append(command,
			validatorName,
			optionAmount,
			selfDelegation,
		)
	case c.sdkVersion.GTE(cosmosver.StargateFortyVersion):
		command = append(command,
			validatorName,
			selfDelegation,
		)
	}

	// Apply the options provided by the user
	for _, apply := range options {
		command = apply(command)
	}

	command = c.attachChainID(command)
	command = c.attachKeyringBackend(command)

	return c.daemonCommand(command)
}

// CollectGentxsCommand returns the command to gather the gentxs in /gentx dir into the genesis file of the chain.
func (c ChainCmd) CollectGentxsCommand() step.Option {
	command := []string{
		commandGenesis,
		commandCollectGentxs,
	}
	return c.daemonCommand(command)
}

// ValidateGenesisCommand returns the command to check the validity of the chain genesis.
func (c ChainCmd) ValidateGenesisCommand() step.Option {
	command := []string{
		commandGenesis,
		commandValidateGenesis,
	}
	return c.daemonCommand(command)
}

// ShowNodeIDCommand returns the command to print the node ID of the node for the chain.
func (c ChainCmd) ShowNodeIDCommand() step.Option {
	command := []string{
		constTendermint,
		commandShowNodeID,
	}
	return c.daemonCommand(command)
}

// UnsafeResetCommand returns the command to reset the blockchain database.
func (c ChainCmd) UnsafeResetCommand() step.Option {
	var command []string

	if c.sdkVersion.GTE(cosmosver.StargateFortyFiveThreeVersion) {
		command = append(command, commandTendermint)
	}

	command = append(command, commandUnsafeReset)

	return c.daemonCommand(command)
}

// ExportCommand returns the command to export the state of the blockchain into a genesis file.
func (c ChainCmd) ExportCommand() step.Option {
	command := []string{
		commandExportGenssis,
	}
	return c.daemonCommand(command)
}

// BankSendOption for the BankSendCommand.
type BankSendOption func([]string) []string

// BankSendWithFees sets fees to pay along with transaction for the bank send command.
func BankSendWithFees(fee sdk.Coin) BankSendOption {
	return func(command []string) []string {
		if !fee.IsNil() {
			return append(command, optionFees, fee.String())
		}
		return command
	}
}

// BankSendCommand returns the command for transferring tokens.
func (c ChainCmd) BankSendCommand(fromAddress, toAddress, amount string, options ...BankSendOption) step.Option {
	command := []string{
		commandTx,
	}

	command = append(command, "bank")
	command = append(command,
		"send",
		fromAddress,
		toAddress,
		amount,
	)
	command = append(command,
		optionBroadcastMode, flags.BroadcastSync,
		optionYes,
	)

	// Apply the options provided by the user
	for _, apply := range options {
		command = apply(command)
	}

	command = c.attachChainID(command)
	command = c.attachKeyringBackend(command)
	command = c.attachNode(command)

	return c.cliCommand(command)
}

// QueryTxCommand returns the command to query tx.
func (c ChainCmd) QueryTxCommand(txHash string) step.Option {
	command := []string{
		commandQuery,
		"tx",
		txHash,
	}

	command = c.attachNode(command)
	return c.cliCommand(command)
}

// QueryTxEventsCommand returns the command to query events.
func (c ChainCmd) QueryTxEventsCommand(query string) step.Option {
	command := []string{
		commandQuery,
		"txs",
		"--query",
		query,
		"--page", "1",
		"--limit", "1000",
		"--output", "json",
	}

	command = c.attachNode(command)
	return c.cliCommand(command)
}

// QueryTxQueryCommand returns the command to query tx.
func (c ChainCmd) QueryTxQueryCommand(query string) step.Option {
	command := []string{
		commandQuery,
		"txs",
		"--query",
		query,
		"--page", "1",
		"--limit", "1000",
		"--output", "json",
	}

	command = c.attachNode(command)
	return c.cliCommand(command)
}

// StatusCommand returns the command that fetches node's status.
func (c ChainCmd) StatusCommand() step.Option {
	command := []string{
		commandStatus,
	}

	command = c.attachNode(command)
	return c.cliCommand(command)
}

// KeyringBackend returns the underlying keyring backend.
func (c ChainCmd) KeyringBackend() KeyringBackend {
	return c.keyringBackend
}

// KeyringPassword returns the underlying keyring password.
func (c ChainCmd) KeyringPassword() string {
	return c.keyringPassword
}

// attachChainID appends the chain ID flag to the provided command.
func (c ChainCmd) attachChainID(command []string) []string {
	if c.chainID != "" {
		command = append(command, []string{optionChainID, c.chainID}...)
	}
	return command
}

// attachKeyringBackend appends the keyring backend flag to the provided command.
func (c ChainCmd) attachKeyringBackend(command []string) []string {
	if c.keyringBackend != "" {
		command = append(command, []string{optionKeyringBackend, string(c.keyringBackend)}...)
	}
	return command
}

// attachHome appends the home flag to the provided command.
func (c ChainCmd) attachHome(command []string) []string {
	if c.homeDir != "" {
		command = append(command, []string{optionHome, c.homeDir}...)
	}
	return command
}

// attachNode appends the node flag to the provided command.
func (c ChainCmd) attachNode(command []string) []string {
	if c.nodeAddress != "" {
		command = append(command, []string{optionNode, c.nodeAddress}...)
	}
	return command
}

// daemonCommand returns the daemon command from the provided command.
func (c ChainCmd) daemonCommand(command []string) step.Option {
	return step.Exec(c.appCmd, c.attachHome(command)...)
}

// cliCommand returns the cli command from the provided command.
func (c ChainCmd) cliCommand(command []string) step.Option {
	return step.Exec(c.appCmd, c.attachHome(command)...)
}

// KeyringBackendFromString returns the keyring backend from its string.
func KeyringBackendFromString(kb string) (KeyringBackend, error) {
	existingKeyringBackend := map[KeyringBackend]bool{
		KeyringBackendUnspecified: true,
		KeyringBackendOS:          true,
		KeyringBackendFile:        true,
		KeyringBackendPass:        true,
		KeyringBackendTest:        true,
		KeyringBackendKwallet:     true,
	}

	if _, ok := existingKeyringBackend[KeyringBackend(kb)]; ok {
		return KeyringBackend(kb), nil
	}
	return KeyringBackendUnspecified, errors.Errorf("unrecognized keyring backend: %s", kb)
}
