// Copyright 2019 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package env

import (
	"errors"
	"path/filepath"
	"strings"

	"github.com/dolthub/dolt/go/libraries/doltcore/dbfactory"
	"github.com/dolthub/dolt/go/libraries/utils/config"
	"github.com/dolthub/dolt/go/libraries/utils/filesys"
	"github.com/dolthub/dolt/go/libraries/utils/set"
	"github.com/dolthub/dolt/go/store/datas"
)

const (
	localConfigName  = "local"
	globalConfigName = "global"

	UserEmailKey = "user.email"
	UserNameKey  = "user.name"

	// should be able to have remote specific creds?
	UserCreds = "user.creds"

	DoltEditor = "core.editor"

	InitBranchName = "init.defaultbranch"

	RemotesApiHostKey     = "remotes.default_host"
	RemotesApiHostPortKey = "remotes.default_port"

	AddCredsUrlKey     = "creds.add_url"
	DoltLabInsecureKey = "doltlab.insecure"

	MetricsDisabled = "metrics.disabled"
	MetricsHost     = "metrics.host"
	MetricsPort     = "metrics.port"
	MetricsInsecure = "metrics.insecure"

	PushAutoSetupRemote = "push.autosetupremote"
)

var LocalConfigWhitelist = set.NewStrSet([]string{UserNameKey, UserEmailKey})
var GlobalConfigWhitelist = set.NewStrSet([]string{UserNameKey, UserEmailKey})

// ConfigScope is an enum representing the elements that make up the ConfigHierarchy
type ConfigScope int

const (
	// LocalConfig is the repository's local config portion of the ConfigHierarchy
	LocalConfig ConfigScope = iota

	// GlobalConfig is the user's global config portion of the ConfigHierarchy
	GlobalConfig
)

const (
	// SqlServerGlobalsPrefix is config namespace accessible by the SQL engine (ex: sqlserver.global.key)
	SqlServerGlobalsPrefix = "sqlserver.global"
)

// String gives the string name of an element that was used when it was added to the ConfigHierarchy, which is the
// same name that is used to retrieve that element of the string hierarchy.
func (ce ConfigScope) String() string {
	switch ce {
	case LocalConfig:
		return localConfigName
	case GlobalConfig:
		return globalConfigName
	}

	return ""
}

// DoltCliConfig is the config for the cli
type DoltCliConfig struct {
	config.ReadableConfig

	ch *config.ConfigHierarchy
	fs filesys.ReadWriteFS
}

var _ config.ReadableConfig = &DoltCliConfig{}

func LoadDoltCliConfig(hdp HomeDirProvider, fs filesys.ReadWriteFS) (*DoltCliConfig, error) {
	ch := config.NewConfigHierarchy()

	lPath := getLocalConfigPath()
	if exists, _ := fs.Exists(lPath); exists {
		lCfg, err := config.FromFile(lPath, fs)

		if err == nil {
			ch.AddConfig(localConfigName, lCfg)
		}
	}

	gPath, err := getGlobalCfgPath(hdp)
	if err != nil {
		return nil, err
	}

	gCfg, err := ensureGlobalConfig(gPath, fs)
	if err != nil {
		return nil, err
	}

	ch.AddConfig(globalConfigName, gCfg)

	return &DoltCliConfig{ch, ch, fs}, nil
}

func ensureGlobalConfig(path string, fs filesys.ReadWriteFS) (config.ReadWriteConfig, error) {
	if exists, isDir := fs.Exists(path); exists {
		if isDir {
			return nil, errors.New("A directory exists where this file should be. path: " + path)
		}

		return config.FromFile(path, fs)
	}

	return config.NewFileConfig(path, fs, map[string]string{})
}

// CreateLocalConfig creates a new repository local config file.  The current directory must have already been initialized
// as a data repository before a local config can be created.
func (dcc *DoltCliConfig) CreateLocalConfig(vals map[string]string) error {
	return dcc.createLocalConfigAt(".", vals)
}

func (dcc *DoltCliConfig) createLocalConfigAt(dir string, vals map[string]string) error {
	doltDir := filepath.Join(dir, dbfactory.DoltDir)
	if exists, isDir := dcc.fs.Exists(doltDir); !exists {
		return errors.New(dbfactory.DoltDir + " directory not found. Is the current directory a repository directory?")
	} else if !isDir {
		return errors.New("A file exists with the name \"" + dbfactory.DoltDir + "\". This is not a valid file within a data repository directory.")
	}

	path := filepath.Join(dir, getLocalConfigPath())
	cfg, err := config.NewFileConfig(path, dcc.fs, vals)

	if err != nil {
		return err
	}

	dcc.ch.AddConfig(localConfigName, cfg)

	return nil
}

// GetConfig retrieves a specific element of the config hierarchy.
func (dcc *DoltCliConfig) GetConfig(element ConfigScope) (config.ReadWriteConfig, bool) {
	switch element {
	case LocalConfig, GlobalConfig:
		return dcc.ch.GetConfig(element.String())
	default:
		return nil, false
	}
}

// GetStringOrDefault retrieves a string from the config hierarchy and returns it if available.  Otherwise it returns
// the default string value
func (dcc *DoltCliConfig) GetStringOrDefault(key, defStr string) string {
	return GetStringOrDefault(dcc.ch, key, defStr)
}

// IfEmptyUseConfig looks at a strings value and if it is an empty string will try to return a value from the config
// hierarchy.  If it is missing in the config a pointer to an empty string will be returned.
func (dcc *DoltCliConfig) IfEmptyUseConfig(val, key string) string {
	if len(strings.TrimSpace(val)) > 0 {
		return val
	}

	cfgVal, err := dcc.ch.GetString(key)

	if err != nil {
		s := ""
		return s
	}

	return cfgVal
}

func GetStringOrDefault(cfg config.ReadableConfig, key, defStr string) string {
	val, err := cfg.GetString(key)

	if err != nil {
		return defStr
	}

	return val
}

// GetNameAndEmail returns the name and email from the supplied config
func GetNameAndEmail(cfg config.ReadableConfig) (string, string, error) {
	name, err := cfg.GetString(UserNameKey)

	if err == config.ErrConfigParamNotFound {
		return "", "", datas.ErrNameNotConfigured
	} else if err != nil {
		return "", "", err
	}

	email, err := cfg.GetString(UserEmailKey)

	if err == config.ErrConfigParamNotFound {
		return "", "", datas.ErrEmailNotConfigured
	} else if err != nil {
		return "", "", err
	}

	return name, email, nil
}

// writeableLocalDoltCliConfig is an extension to DoltCliConfig that reads values from the hierarchy but writes to
// local config.
type writeableLocalDoltCliConfig struct {
	*DoltCliConfig
}

// WriteableConfig returns a ReadWriteConfig reading from this config hierarchy. The config will read from the hierarchy
// and write to the local config if it's available, or the global config otherwise.
func (dcc *DoltCliConfig) WriteableConfig() config.ReadWriteConfig {
	return writeableLocalDoltCliConfig{dcc}
}

// SetFailsafes sets the config values given as failsafes, i.e. values that will be returned as a last resort if they
// are not found elsewhere in the config hierarchy. The "failsafe" config can be written to in order to conform to the
// interface of ConfigHierarchy, but values will not persist beyond this session.
// Calling SetFailsafes more than once will overwrite any previous values.
// Should only be called after primary configuration of the config hierarchy has been completed.
func (dcc DoltCliConfig) SetFailsafes(cfg map[string]string) {
	existing, ok := dcc.ch.GetConfig("failsafe")
	if !ok {
		existing = config.NewEmptyMapConfig()
		dcc.ch.AddConfig("failsafe", existing)
	}

	_ = existing.SetStrings(cfg)
}

const (
	DefaultEmail = "doltuser@dolthub.com"
	DefaultName  = "Dolt System Account"
)

var DefaultFailsafeConfig = map[string]string{
	UserEmailKey: DefaultEmail,
	UserNameKey:  DefaultName,
}

func (w writeableLocalDoltCliConfig) SetStrings(updates map[string]string) error {
	cfg, ok := w.GetConfig(LocalConfig)
	if !ok {
		cfg, ok = w.GetConfig(GlobalConfig)
		if !ok {
			return errors.New("no local or global config found")
		}
	}

	return cfg.SetStrings(updates)
}

func (w writeableLocalDoltCliConfig) Unset(params []string) error {
	cfg, ok := w.GetConfig(LocalConfig)
	if !ok {
		cfg, ok = w.GetConfig(GlobalConfig)
		if !ok {
			return errors.New("no local or global config found")
		}
	}

	return cfg.Unset(params)
}
