package chain

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/go-git/go-git/v5"
	"github.com/spf13/cobra"

	chainconfig "github.com/ignite/cli/v29/ignite/config/chain"
	chainconfigv1 "github.com/ignite/cli/v29/ignite/config/chain/v1"
	"github.com/ignite/cli/v29/ignite/pkg/chaincmd"
	chaincmdrunner "github.com/ignite/cli/v29/ignite/pkg/chaincmd/runner"
	"github.com/ignite/cli/v29/ignite/pkg/cliui/colors"
	uilog "github.com/ignite/cli/v29/ignite/pkg/cliui/log"
	"github.com/ignite/cli/v29/ignite/pkg/confile"
	"github.com/ignite/cli/v29/ignite/pkg/cosmosaccount"
	"github.com/ignite/cli/v29/ignite/pkg/cosmosanalysis"
	"github.com/ignite/cli/v29/ignite/pkg/cosmosver"
	"github.com/ignite/cli/v29/ignite/pkg/errors"
	"github.com/ignite/cli/v29/ignite/pkg/events"
	"github.com/ignite/cli/v29/ignite/pkg/repoversion"
	"github.com/ignite/cli/v29/ignite/pkg/xexec"
	"github.com/ignite/cli/v29/ignite/pkg/xurl"
	igniteversion "github.com/ignite/cli/v29/ignite/version"
)

const (
	flagPath = "path"
	flagHome = "home"
)

type (
	// Chain provides programmatic access and tools for a Cosmos SDK blockchain.
	Chain struct {
		// app holds info about blockchain app.
		app App

		options chainOptions

		Version cosmosver.Version

		sourceVersion  version
		serveCancel    context.CancelFunc
		serveRefresher chan struct{}
		served         bool

		ev          events.Bus
		logOutputer uilog.Outputer
	}

	// chainOptions holds user given options that overwrites chain's defaults.
	chainOptions struct {
		// chainID is the chain's id.
		chainID string

		// homePath of the chain's config dir.
		homePath string

		// keyring backend used by commands if not specified in configuration
		keyringBackend chaincmd.KeyringBackend

		// checkDependencies checks that cached Go dependencies of the chain have not
		// been modified since they were downloaded.
		checkDependencies bool

		// checkCosmosSDKVersion checks that the app was scaffolded with version of
		// the Cosmos SDK that is supported by Ignite CLI.
		checkCosmosSDKVersion bool

		// printGeneratedPaths prints the output paths of the generated code
		printGeneratedPaths bool

		// path of a custom config file
		ConfigFile string
	}

	version struct {
		tag  string
		hash string
	}

	// Option configures Chain.
	Option func(*Chain)
)

// ID replaces chain's id with given id.
func ID(id string) Option {
	return func(c *Chain) {
		c.options.chainID = id
	}
}

// HomePath replaces chain's configuration home path with given path.
func HomePath(path string) Option {
	return func(c *Chain) {
		c.options.homePath = path
	}
}

// KeyringBackend specifies the keyring backend to use for the chain command.
func KeyringBackend(keyringBackend chaincmd.KeyringBackend) Option {
	return func(c *Chain) {
		c.options.keyringBackend = keyringBackend
	}
}

// ConfigFile specifies a custom config file to use.
func ConfigFile(configFile string) Option {
	return func(c *Chain) {
		c.options.ConfigFile = configFile
	}
}

// WithOutputer sets the CLI outputer for the chain.
func WithOutputer(s uilog.Outputer) Option {
	return func(c *Chain) {
		c.logOutputer = s
	}
}

// CollectEvents collects events from the chain.
func CollectEvents(ev events.Bus) Option {
	return func(c *Chain) {
		c.ev = ev
	}
}

// CheckDependencies checks that cached Go dependencies of the chain have not
// been modified since they were downloaded. Dependencies are checked by
// running `go mod verify`.
func CheckDependencies() Option {
	return func(c *Chain) {
		c.options.checkDependencies = true
	}
}

// CheckCosmosSDKVersion checks that the app was scaffolded with a version of
// the Cosmos SDK that is supported by Ignite CLI.
func CheckCosmosSDKVersion() Option {
	return func(c *Chain) {
		c.options.checkCosmosSDKVersion = true
	}
}

// PrintGeneratedPaths prints the output paths of the generated code.
func PrintGeneratedPaths() Option {
	return func(c *Chain) {
		c.options.printGeneratedPaths = true
	}
}

// New initializes a new Chain with options that its source lives at path.
func New(path string, options ...Option) (*Chain, error) {
	app, err := NewAppAt(path)
	if err != nil {
		return nil, err
	}

	c := &Chain{
		app:            app,
		serveRefresher: make(chan struct{}, 1),
	}

	// Apply the options
	for _, apply := range options {
		apply(c)
	}

	c.sourceVersion, err = c.appVersion()
	if err != nil && !errors.Is(err, git.ErrRepositoryNotExists) {
		return nil, err
	}

	c.Version, err = cosmosver.Detect(c.app.Path)
	if err != nil {
		return nil, err
	}

	if c.options.checkCosmosSDKVersion {
		if err := igniteversion.AssertSupportedCosmosSDKVersion(c.Version); err != nil {
			return nil, err
		}
	}

	return c, nil
}

func NewWithHomeFlags(cmd *cobra.Command, chainOption ...Option) (*Chain, error) {
	var (
		home, _    = cmd.Flags().GetString(flagHome)
		appPath, _ = cmd.Flags().GetString(flagPath)
	)

	absPath, err := filepath.Abs(appPath)
	if err != nil {
		return nil, err
	}

	// Check if custom home is provided
	if home != "" {
		chainOption = append(chainOption, HomePath(home))
	}
	return New(absPath, chainOption...)
}

func (c *Chain) appVersion() (v version, err error) {
	ver, err := repoversion.Determine(c.app.Path)
	if err != nil {
		return version{}, err
	}

	v.hash = ver.Hash
	v.tag = ver.Tag

	return v, nil
}

// RPCPublicAddress points to the public address of Tendermint RPC, this is shared by
// other chains for relayer related actions.
func (c *Chain) RPCPublicAddress() (string, error) {
	conf, err := c.Config()
	if err != nil {
		return "", err
	}

	validator, err := chainconfig.FirstValidator(conf)
	if err != nil {
		return "", err
	}

	servers, err := validator.GetServers()
	if err != nil {
		return "", err
	}

	return servers.RPC.Address, nil
}

// ConfigPath returns the config path of the chain.
// Empty string means that the chain has no defined config.
func (c *Chain) ConfigPath() string {
	if c.options.ConfigFile != "" {
		return c.options.ConfigFile
	}
	path, err := chainconfig.LocateDefault(c.app.Path)
	if err != nil {
		return ""
	}
	return path
}

// Config returns the config of the chain.
func (c *Chain) Config() (*chainconfig.Config, error) {
	configPath := c.ConfigPath()
	if configPath == "" {
		return chainconfig.DefaultChainConfig(), nil
	}
	return chainconfig.ParseFile(configPath)
}

// ID returns the chain's id.
func (c *Chain) ID() (string, error) {
	// chainID in App has the most priority.
	if c.options.chainID != "" {
		return c.options.chainID, nil
	}

	// otherwise uses defined in config.yml
	chainConfig, err := c.Config()
	if err != nil {
		return "", err
	}
	genid, ok := chainConfig.Genesis["chain_id"]
	if ok {
		return genid.(string), nil
	}

	// use app name by default.
	return c.app.N(), nil
}

// Name returns the chain's name.
func (c *Chain) Name() string {
	return c.app.N()
}

// Binary returns the name of app's default (appd) binary.
func (c *Chain) Binary() (string, error) {
	conf, err := c.Config()
	if err != nil {
		return "", err
	}

	if conf.Build.Binary != "" {
		return conf.Build.Binary, nil
	}

	return c.app.D(), nil
}

// AbsBinaryPath returns the absolute path to the app's binary.
// Returned path includes the binary name.
func (c *Chain) AbsBinaryPath() (string, error) {
	bin, err := c.Binary()
	if err != nil {
		return "", err
	}

	return xexec.ResolveAbsPath(bin)
}

// SetHome sets the chain home directory.
func (c *Chain) SetHome(home string) {
	c.options.homePath = home
}

// Home returns the blockchain node's home dir.
func (c *Chain) Home() (string, error) {
	// check if home is explicitly defined for the app
	home := c.options.homePath
	if home == "" {
		// return default home otherwise
		var err error
		home, err = c.DefaultHome()
		if err != nil {
			return "", err
		}

	}

	// expand environment variables in home
	home = os.ExpandEnv(home)

	return home, nil
}

// AppPath returns the configured App's path.
func (c *Chain) AppPath() string {
	return c.app.Path
}

// DefaultHome returns the blockchain node's default home dir when not specified in the app.
func (c *Chain) DefaultHome() (string, error) {
	// check if home is defined in config
	cfg, err := c.Config()
	if err != nil {
		return "", err
	}
	validator, _ := chainconfig.FirstValidator(cfg)
	if validator.Home != "" {
		expandedHome, err := expandHome(validator.Home)
		if err != nil {
			return "", err
		}
		validator.Home = expandedHome
		return validator.Home, nil
	}

	return c.appHome(), nil
}

// DefaultGentxPath returns default gentx.json path of the app.
func (c *Chain) DefaultGentxPath() (string, error) {
	home, err := c.Home()
	if err != nil {
		return "", err
	}
	return filepath.Join(home, "config/gentx/gentx.json"), nil
}

// GenesisPath returns genesis.json path of the app.
func (c *Chain) GenesisPath() (string, error) {
	home, err := c.Home()
	if err != nil {
		return "", err
	}
	return filepath.Join(home, "config/genesis.json"), nil
}

// GentxsPath returns the directory where gentxs are stored for the app.
func (c *Chain) GentxsPath() (string, error) {
	home, err := c.Home()
	if err != nil {
		return "", err
	}
	return filepath.Join(home, "config/gentx"), nil
}

// AppTOMLPath returns app.toml path of the app.
func (c *Chain) AppTOMLPath() (string, error) {
	home, err := c.Home()
	if err != nil {
		return "", err
	}
	return filepath.Join(home, "config/app.toml"), nil
}

// ConfigTOMLPath returns config.toml path of the app.
func (c *Chain) ConfigTOMLPath() (string, error) {
	home, err := c.Home()
	if err != nil {
		return "", err
	}
	return filepath.Join(home, "config/config.toml"), nil
}

// ClientTOMLPath returns client.toml path of the app.
func (c *Chain) ClientTOMLPath() (string, error) {
	home, err := c.Home()
	if err != nil {
		return "", err
	}
	return filepath.Join(home, "config/client.toml"), nil
}

// KeyringBackend returns the keyring backend chosen for the chain.
func (c *Chain) KeyringBackend() (chaincmd.KeyringBackend, error) {
	// When keyring backend is initialized as a chain
	// option it overrides any configured backends.
	if c.options.keyringBackend != "" {
		return c.options.keyringBackend, nil
	}

	// Try to get keyring backend from the first configured validator
	cfg, err := c.Config()
	if err != nil {
		return "", err
	}

	validator, _ := chainconfig.FirstValidator(cfg)
	if validator.Client != nil {
		if v, ok := validator.Client["keyring-backend"]; ok {
			if backend, ok := v.(string); ok {
				return chaincmd.KeyringBackendFromString(backend)
			}
		}
	}

	// Try to get keyring backend from client.toml config file
	configTOMLPath, err := c.ClientTOMLPath()
	if err != nil {
		return "", err
	}
	cf := confile.New(confile.DefaultTOMLEncodingCreator, configTOMLPath)
	var conf struct {
		KeyringBackend string `toml:"keyring-backend"`
	}
	if err := cf.Load(&conf); err != nil {
		return "", err
	}
	if conf.KeyringBackend != "" {
		return chaincmd.KeyringBackendFromString(conf.KeyringBackend)
	}

	// Use test backend as default when none is configured
	return chaincmd.KeyringBackendTest, nil
}

// Commands returns the runner execute commands on the chain's binary.
func (c *Chain) Commands(ctx context.Context) (chaincmdrunner.Runner, error) {
	id, err := c.ID()
	if err != nil {
		return chaincmdrunner.Runner{}, err
	}

	home, err := c.Home()
	if err != nil {
		return chaincmdrunner.Runner{}, err
	}

	binary, err := c.Binary()
	if err != nil {
		return chaincmdrunner.Runner{}, err
	}

	// Try to make the binary path absolute. This will also
	// find the binary path when the Go bin path is not part
	// of the PATH environment variable.
	binary = xexec.TryResolveAbsPath(binary)

	backend, err := c.KeyringBackend()
	if err != nil {
		return chaincmdrunner.Runner{}, err
	}

	cfg, err := c.Config()
	if err != nil {
		return chaincmdrunner.Runner{}, err
	}

	servers := chainconfigv1.DefaultServers()
	if len(cfg.Validators) > 0 {
		validator, _ := chainconfig.FirstValidator(cfg)
		servers, err = validator.GetServers()
		if err != nil {
			return chaincmdrunner.Runner{}, err
		}
	}

	nodeAddr, err := xurl.TCP(servers.RPC.Address)
	if err != nil {
		return chaincmdrunner.Runner{}, err
	}

	chainCommandOptions := []chaincmd.Option{
		chaincmd.WithChainID(id),
		chaincmd.WithHome(home),
		chaincmd.WithVersion(c.Version),
		chaincmd.WithNodeAddress(nodeAddr),
		chaincmd.WithKeyringBackend(backend),
	}

	cc := chaincmd.New(binary, chainCommandOptions...)

	ccrOptions := []chaincmdrunner.Option{}

	// Enable command output only when CLI verbosity is enabled
	if c.logOutputer != nil && c.logOutputer.Verbosity() == uilog.VerbosityVerbose {
		out := c.logOutputer.NewOutput(c.app.D(), colors.Cyan)
		ccrOptions = append(
			ccrOptions,
			chaincmdrunner.Stdout(out.Stdout()),
			chaincmdrunner.Stderr(out.Stderr()),
		)
	}

	return chaincmdrunner.New(ctx, cc, ccrOptions...)
}

func appBackendSourceWatchPaths(protoDir string) []string {
	return []string{
		"app",
		"cmd",
		"x",
		"third_party",
		protoDir,
	}
}

// expandHome expands a path that may start with "~" and may contain environment variables.
func expandHome(path string) (string, error) {
	if strings.HasPrefix(path, "~") {
		home, err := os.UserHomeDir()
		if err != nil {
			return "", err
		}
		// Only replace the first occurrence at the start.
		path = home + strings.TrimPrefix(path, "~")
	}
	return os.ExpandEnv(path), nil
}

// Bech32Prefix returns the bech32 prefix of the chain.
func (c *Chain) Bech32Prefix() (string, error) {
	prefix, err := c.parseAddressPrefix()
	if err != nil || prefix == "" {
		return cosmosaccount.AccountPrefixCosmos, err
	}

	return prefix, nil
}

// CoinType returns the coin type of the chain.
func (c *Chain) CoinType() (uint32, error) {
	coinType, err := c.parseCoinType()
	if err != nil || coinType == 0 {
		return cosmosaccount.CoinTypeCosmos, err
	}

	return coinType, nil
}

// parseAddressPrefix parses the address prefix from the app code.
func (c *Chain) parseAddressPrefix() (string, error) {
	appGoPath, err := c.findAppGoFile()
	if err != nil {
		return "", err
	}

	content, err := os.ReadFile(appGoPath)
	if err != nil {
		return "", err
	}

	// try to find the AccountAddressPrefix constant
	lines := strings.SplitSeq(string(content), "\n")
	for line := range lines {
		// match both formats:
		// AccountAddressPrefix = "cosmos"
		// AccountAddressPrefix string = "cosmos"
		if strings.Contains(line, "AccountAddressPrefix") && strings.Contains(line, "=") {
			parts := strings.Split(line, "=")
			if len(parts) < 2 {
				continue
			}

			// extract the value within quotes
			value := strings.TrimSpace(parts[1])
			// remove comments if any
			if idx := strings.Index(value, "//"); idx >= 0 {
				value = value[:idx]
			}
			value = strings.TrimSpace(value)

			// extract string between quotes
			if start := strings.Index(value, "\""); start >= 0 {
				if end := strings.Index(value[start+1:], "\""); end >= 0 {
					return value[start+1 : start+1+end], nil
				}
			}
		}
	}

	return "", nil
}

// parseCoinType parses the coin type from the app code.
func (c *Chain) parseCoinType() (uint32, error) {
	appGoPath, err := c.findAppGoFile()
	if err != nil {
		return 0, err
	}

	content, err := os.ReadFile(appGoPath)
	if err != nil {
		return 0, err
	}

	// try to find the ChainCoinType constant
	lines := strings.SplitSeq(string(content), "\n")
	for line := range lines {
		if strings.Contains(line, "ChainCoinType") && strings.Contains(line, "=") {
			parts := strings.Split(line, "=")
			if len(parts) < 2 {
				continue
			}

			// extract the numeric value
			value := strings.TrimSpace(parts[1])
			// remove comments if any
			if idx := strings.Index(value, "//"); idx >= 0 {
				value = value[:idx]
			}
			value = strings.TrimSpace(value)

			// parse the value as uint32
			var coinType uint32
			if _, err := fmt.Sscanf(value, "%d", &coinType); err == nil {
				return coinType, nil
			}
		}
	}

	return 0, nil
}

// findAppGoFile attempts to find the app.go file in the project.
func (c *Chain) findAppGoFile() (string, error) {
	// Look for the app.go file in common locations
	commonPath := filepath.Join(c.app.Path, "app", "app.go")
	if _, err := os.Stat(commonPath); err == nil {
		return commonPath, nil
	}

	return cosmosanalysis.FindAppFilePath(c.app.Path)
}
