package config

import (
	"errors"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"strings"

	"github.com/knadh/koanf/providers/env"
	"github.com/knadh/koanf/providers/posflag"
	"github.com/knadh/koanf/v2"
	"github.com/spf13/pflag"

	"github.com/stashapp/stash/pkg/fsutil"
	"github.com/stashapp/stash/pkg/logger"
)

type flagStruct struct {
	configFilePath string
	nobrowser      bool
}

var (
	flags flagStruct

	homeDir, _ = os.UserHomeDir()

	defaultConfigLocations = []string{
		"config.yml",
		filepath.Join(homeDir, ".stash", "config.yml"),
	}

	// map of env vars to config keys
	envBinds = map[string]string{
		"host":          Host,
		"port":          Port,
		"external_host": ExternalHost,
		"generated":     Generated,
		"metadata":      Metadata,
		"blobs":         BlobsPath,
		"cache":         Cache,
		"stash":         Stash,
		"ui":            UILocation,
	}
)

var errConfigNotFound = errors.New("config file not found")

func init() {
	pflag.IP("host", net.IPv4(0, 0, 0, 0), "ip address for the host")
	pflag.Int("port", 9999, "port to serve from")
	pflag.StringVarP(&flags.configFilePath, "config", "c", "", "config file to use")
	pflag.BoolVar(&flags.nobrowser, "nobrowser", false, "Don't open a browser window after launch")
	pflag.StringP("ui-location", "u", "", "path to the webui")
}

// Called at startup
func Initialize() (*Config, error) {
	cfg := &Config{
		main:      koanf.New("."),
		overrides: koanf.New("."),
	}

	cfg.initOverrides()

	err := cfg.initConfig()
	if err != nil {
		return nil, err
	}

	if cfg.isNewSystem {
		if cfg.Validate() == nil {
			// system has been initialised by the environment
			cfg.isNewSystem = false
		}
	}

	if !cfg.isNewSystem {
		cfg.setExistingSystemDefaults()

		err := cfg.SetInitialConfig()
		if err != nil {
			return nil, err
		}

		err = cfg.Write()
		if err != nil {
			return nil, err
		}

		err = cfg.Validate()
		if err != nil {
			return nil, err
		}
	}

	instance = cfg
	return instance, nil
}

// Called by tests to initialize an empty config
func InitializeEmpty() *Config {
	cfg := &Config{
		main:      koanf.New("."),
		overrides: koanf.New("."),
	}
	instance = cfg
	return instance
}

func (i *Config) loadFromCommandLine() {
	v := i.overrides

	if err := v.Load(posflag.ProviderWithFlag(pflag.CommandLine, ".", v, func(f *pflag.Flag) (string, interface{}) {
		// ignore flags that have not been changed
		if !f.Changed {
			return "", nil
		}

		return f.Name, posflag.FlagVal(pflag.CommandLine, f)
	}), nil); err != nil {
		logger.Errorf("failed to load flags: %v", err)
	}
}

func (i *Config) loadFromEnv() {
	v := i.overrides

	if err := v.Load(env.ProviderWithValue("STASH_", ".", func(key, value string) (string, interface{}) {
		key = strings.ToLower(strings.TrimPrefix(key, "STASH_"))
		if newKey, ok := envBinds[key]; ok {
			return newKey, value
		}

		return "", nil
	}), nil); err != nil {
		logger.Errorf("failed to load envs: %v", err)
	}
}

func (i *Config) initOverrides() {
	i.loadFromCommandLine()
	i.loadFromEnv()
}

func (i *Config) initConfig() error {
	configFile := ""
	envConfigFile := os.Getenv("STASH_CONFIG_FILE")

	if flags.configFilePath != "" {
		configFile = flags.configFilePath
	} else if envConfigFile != "" {
		configFile = envConfigFile
	}

	if configFile != "" {
		// if file does not exist, assume it is a new system
		if exists, _ := fsutil.FileExists(configFile); !exists {
			i.isNewSystem = true
			i.SetConfigFile(configFile)

			// ensure we can write to the file
			if err := fsutil.Touch(configFile); err != nil {
				return fmt.Errorf(`could not write to provided config path "%s": %v`, configFile, err)
			} else {
				// remove the file
				os.Remove(configFile)
			}

			return nil
		} else {
			// load from provided config file
			if err := i.loadFirstFromFiles([]string{configFile}); err != nil {
				return err
			}
		}
	} else {
		// load from default locations
		if err := i.loadFirstFromFiles(defaultConfigLocations); err != nil {
			if errors.Is(err, errConfigNotFound) {
				i.isNewSystem = true
				return nil
			}

			return err
		}
	}

	return nil
}

func (i *Config) loadFirstFromFiles(f []string) error {
	for _, ff := range f {
		if exists, _ := fsutil.FileExists(ff); exists {
			return i.load(ff)
		}
	}

	return errConfigNotFound
}
