/*
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>

Scotty is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later version.

Scotty is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with
Scotty. If not, see <https://www.gnu.org/licenses/>.
*/

package config

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

	"github.com/pelletier/go-toml/v2"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	"go.uploadedlobster.com/scotty/internal/i18n"
	"go.uploadedlobster.com/scotty/internal/version"
)

const (
	defaultDatabase  = "scotty.sqlite3"
	defaultOAuthHost = "127.0.0.1:2369"
	fileMode         = 0640
)

func DefaultConfigDir() string {
	configDir, err := os.UserConfigDir()
	cobra.CheckErr(err)
	return path.Join(configDir, version.AppName)
}

// initConfig reads in config file and ENV variables if set.
func InitConfig(cfgFile string) error {
	configDir := DefaultConfigDir()
	if cfgFile != "" {
		// Use given config file
		viper.SetConfigFile(cfgFile)
	} else {
		viper.AddConfigPath(configDir)
		viper.SetConfigType("toml")
		viper.SetConfigName(version.AppName)
		viper.SetConfigPermissions(fileMode)
	}

	setDefaults()

	// Create global config if it does not exist
	if viper.ConfigFileUsed() == "" && cfgFile == "" {
		if err := os.MkdirAll(configDir, 0750); err == nil {
			// This call is expected to return an error if the file already exists
			viper.SafeWriteConfig() //nolint:errcheck
		}
	}

	// read in environment variables that match
	viper.AutomaticEnv()

	// If a config file is found, read it in.
	return viper.ReadInConfig()
}

// Write the configuration except for removedKeys
func WriteConfig(removedKeys ...string) error {
	file := viper.ConfigFileUsed()
	if len(file) == 0 {
		return errors.New(i18n.Tr("no configuration file defined, cannot write config"))
	}

	configMap := viper.AllSettings()
	for _, key := range removedKeys {
		c := configMap
		var ok bool
		subKeys := strings.Split(key, ".")
		keyLen := len(subKeys)
		// Deep search the key in the config and delete the deepest key, if it exists
		for i, s := range subKeys {
			if i == keyLen-1 {
				// This is the final key, delete it from the map
				delete(c, s)
			} else {
				// Use the child for next iteration if it is a map
				c, ok = c[s].(map[string]any)
				if !ok {
					// Child is not a map, can't search deeper
					break
				}
			}
		}
	}

	content, err := toml.Marshal(configMap)
	if err != nil {
		return err
	}

	return os.WriteFile(file, content, fileMode)
}

func DatabasePath() string {
	path := viper.GetString("database")
	if filepath.IsAbs(path) {
		return path
	}

	return filepath.Join(getConfigDir(), path)
}

func ValidateKey(key string) error {
	found, err := regexp.MatchString("^[A-Za-z0-9_-]+$", key)
	if err != nil {
		return err
	} else if found {
		return nil
	} else {
		return fmt.Errorf(i18n.Tr("key must only consist of A-Za-z0-9_-"))
	}
}

func setDefaults() {
	viper.SetDefault("database", defaultDatabase)
	viper.SetDefault("oauth-host", defaultOAuthHost)

	// Always configure the dump backend as a default service
	viper.SetDefault("service.dump.backend", "dump")
}

func getConfigDir() string {
	return filepath.Dir(viper.ConfigFileUsed())
}