package config

import (
	"fmt"
	"os"
	"path/filepath"
	"reflect"
	"sort"
	"strconv"
	"strings"

	"gopkg.in/yaml.v2"
	"knative.dev/func/pkg/builders"
	fn "knative.dev/func/pkg/functions"
	"knative.dev/func/pkg/k8s"
)

const (
	// Filename into which Config is serialized
	Filename = "config.yaml"

	// Repositories is the default directory for repositoires.
	Repositories = "repositories"

	// DefaultLanguage is intentionaly undefined.
	DefaultLanguage = ""

	// DefaultBuilder is statically defined by the builders package.
	DefaultBuilder = builders.Default
)

// Global configuration settings.
type Global struct {
	Builder   string `yaml:"builder,omitempty"`
	Confirm   bool   `yaml:"confirm,omitempty"`
	Language  string `yaml:"language,omitempty"`
	Namespace string `yaml:"namespace,omitempty"`
	Registry  string `yaml:"registry,omitempty"`
	Verbose   bool   `yaml:"verbose,omitempty"`
	// NOTE: all members must include their yaml serialized names, even when
	// this is the default, because these tag values are used for the static
	// getter/setter accessors to match requests.

	RegistryInsecure bool `yaml:"registryInsecure,omitempty"`
}

// New Config struct with all members set to static defaults.  See NewDefaults
// for one which further takes into account the optional config file.
func New() Global {
	return Global{
		Builder:  DefaultBuilder,
		Language: DefaultLanguage,
		// ...
	}
}

// RegistyDefault is a convenience method for deferred calculation of a
// default registry taking into account both the global config file and cluster
// detection.
func (c Global) RegistryDefault() string {
	// If defined, the user's choice for global registry default value is used
	if c.Registry != "" {
		return c.Registry
	}
	switch {
	case k8s.IsOpenShift():
		return k8s.GetDefaultOpenShiftRegistry()
	default:
		return ""
	}
}

// NewDefault returns a config populated by global defaults as defined by the
// config file located in .Path() (the global func settings path, which is
//
//	usually ~/.config/func).
//
// The config path is not required to be present.
func NewDefault() (cfg Global, err error) {
	cfg = New()
	cp := File()
	bb, err := os.ReadFile(cp)
	if err != nil {
		// config file is not required
		// permissions warning printed in cmd/root.go as to not spam here
		// TODO: gauron99 - review the whole process for simplification
		if os.IsNotExist(err) || os.IsPermission(err) {
			err = nil
		}
		return
	}
	err = yaml.Unmarshal(bb, &cfg) // cfg now has applied config.yaml
	return
}

// Load the config exactly as it exists at path (no static defaults)
func Load(path string) (c Global, err error) {
	bb, err := os.ReadFile(path)
	if err != nil {
		return c, fmt.Errorf("error reading global config: %v", err)
	}
	err = yaml.Unmarshal(bb, &c)
	return
}

// Write the config to the given path
// To use the currently configured path (used by the constructor) use File()
//
//	c := config.NewDefault()
//	c.Verbose = true
//	c.Write(config.File())
func (c Global) Write(path string) (err error) {
	bb, _ := yaml.Marshal(&c) // Marshaling no longer errors; this is back compat
	return os.WriteFile(path, bb, os.ModePerm)
}

// Apply populated values from a function to the config.
// The resulting config is global settings overridden by a given function.
func (c Global) Apply(f fn.Function) Global {
	// With no way to automate this mapping easily (even with reflection) because
	// the function now has a complex structure consiting of XSpec sub-structs,
	// and in some cases has differing member names (language).  While this is
	// yes a bit tedious, manually mapping each member (if defined) is simple,
	// easy to understand and support; with both mapping direction (Apply and
	// Configure) in one central place here... with tests.
	if f.Build.Builder != "" {
		c.Builder = f.Build.Builder
	}
	if f.Runtime != "" {
		c.Language = f.Runtime
	}
	// Namespace resolution is handled manually in the CLI due to needing
	// to consider current kubernetes context.  This may be merged back
	// in here once the logic is refined.
	// if f.Deploy.Namespace != "" {
	// 	c.Namespace = f.Deploy.Namespace
	// }
	if f.Registry != "" {
		c.Registry = f.Registry
	}
	return c
}

// Configure a function with populated values of the config.
// The resulting function is the function overridden by values on config.
func (c Global) Configure(f fn.Function) fn.Function {
	if c.Builder != "" {
		f.Build.Builder = c.Builder
	}
	if c.Language != "" {
		f.Runtime = c.Language
	}
	if c.Namespace != "" {
		f.Deploy.Namespace = c.Namespace
	}
	if c.Registry != "" {
		f.Registry = c.Registry
	}
	return f
}

// Dir is derived in the following order, from lowest
// to highest precedence.
//  1. The default path is the zero value, indicating "no config path available",
//     and users of this package should act accordingly.
//  2. ~/.config/func if it exists (can be expanded: user has a home dir)
//  3. The value of $XDG_CONFIG_PATH/func if the environment variable exists.
//
// The path is created if it does not already exist.
func Dir() (path string) {
	// Use home if available
	if home, err := os.UserHomeDir(); err == nil {
		path = filepath.Join(home, ".config", "func")
	}

	// 'XDG_CONFIG_HOME/func' takes precedence if defined
	if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
		path = filepath.Join(xdg, "func")
	}

	return
}

// File returns the full path at which to look for a config file.
// Use FUNC_CONFIG_FILE to override default.
func File() string {
	path := filepath.Join(Dir(), Filename)
	if e := os.Getenv("FUNC_CONFIG_FILE"); e != "" {
		path = e
	}
	return path
}

// RepositoriesPath returns the full path at which to look for repositories.
// Use FUNC_REPOSITORIES_PATH to override default.
func RepositoriesPath() string {
	path := filepath.Join(Dir(), Repositories)
	if e := os.Getenv("FUNC_REPOSITORIES_PATH"); e != "" {
		path = e
	}
	return path
}

// CreatePaths is a convenience function for creating the on-disk func config
// structure.  All operations should be tolerant of nonexistant disk
// footprint where possible (for example listing repositories should not
// require an extant path, but _adding_ a repository does require that the func
// config structure exist.
// Current structure is:
// ~/.config/func
// ~/.config/func/repositories
func CreatePaths() (err error) {
	if err = os.MkdirAll(Dir(), os.ModePerm); err != nil {
		return fmt.Errorf("error creating global config path: %v", err)
	}
	if err = os.MkdirAll(RepositoriesPath(), os.ModePerm); err != nil {
		return fmt.Errorf("error creating global config repositories path: %v", err)
	}
	return
}

// Static Accessors
//
// Accessors to globally configurable options are implemented as static
// package functions to retain the benefits of pass-by-value already in use
// on most system structures.
//   c = config.Set(c, "key", "value")
//
// This may initially seem confusing to those used to accessors implemented
// as object member functions (`c.Set("x","y"`), but it appears to be worth it:
//
// This method follows the familiar paradigm of other Go builtins (which made
// their choice for similar reasons):
//   args = append(args, "newarg")
//    ... and thus likewise:
//   c = config.Set(c, "key", "value")
//
// We indeed could have implemented these in a more familiar Getter/Setter way
// by making this method have a pointer receiver:
//     func (c *Global) Set(key, value string)
// However, this would require the user of the config object to, likely more
// confusingly, declare a new local variable to hold their pointer
// (or perhaps worse, abandon the benefits of pass-by-value from the config
// constructor and always return a pointer from the constructor):
//
//   globalConfig := config.NewDefault()
//    .... later that day ...
//   c := &globalConfig
//   c.Set("builder", "foo")
//
// This solution, while it would preserve the very narrow familiar usage of
// 'Set', fails to be clear due to the setup involved (requiring the allocation
// of that pointer). Therefore the accessors are herein implemented more
// functionally, as package static methods.

// List the globally configurable settings by the key which can be used
// in the accessors Get and Set, and in the associated disk serialized.
// Sorted.
// Implemented as a package-static function because Set is implemented as such.
// See the long-winded explanation above.
func List() []string {
	keys := []string{}
	t := reflect.TypeOf(Global{})
	for i := 0; i < t.NumField(); i++ {
		tt := strings.Split(t.Field(i).Tag.Get("yaml"), ",")
		keys = append(keys, tt[0])
	}
	sort.Strings(keys)
	return keys
}

// Get the named global config value from the given global config struct.
// Nonexistent values return nil.
// Implemented as a package-static function because Set is implemented as such.
// See the long-winded explanation above.
func Get(c Global, name string) any {
	t := reflect.TypeOf(c)
	for i := 0; i < t.NumField(); i++ {
		if !strings.HasPrefix(t.Field(i).Tag.Get("yaml"), name) {
			continue
		}
		return reflect.ValueOf(c).FieldByName(t.Field(i).Name).Interface()
	}
	return nil
}

// Set value of a member by name and a stringified value.
// Fails if the passed value can not be coerced into the value expected
// by the member indicated by name.
func Set(c Global, name, value string) (Global, error) {
	fieldValue, err := getField(&c, name)
	if err != nil {
		return c, err
	}

	var v reflect.Value
	switch fieldValue.Kind() {
	case reflect.String:
		v = reflect.ValueOf(value)
	case reflect.Bool:
		boolValue, err := strconv.ParseBool(value)
		if err != nil {
			return c, err
		}
		v = reflect.ValueOf(boolValue)
	default:
		return c, fmt.Errorf("global config value type not yet implemented: %v", fieldValue.Kind())
	}
	fieldValue.Set(v)

	return c, nil
}

// SetString value of a member by name, returning the updated config.
func SetString(c Global, name, value string) (Global, error) {
	return set(c, name, reflect.ValueOf(value))
}

// SetBool value of a member by name, returning the updated config.
func SetBool(c Global, name string, value bool) (Global, error) {
	return set(c, name, reflect.ValueOf(value))
}

// TODO: add more typesafe setters as needed.

// set using a reflect.Value
func set(c Global, name string, value reflect.Value) (Global, error) {
	fieldValue, err := getField(&c, name)
	if err != nil {
		return c, err
	}
	fieldValue.Set(value)
	return c, nil
}

// Get an assignable reflect.Value for the struct field with the given yaml
// tag name.
func getField(c *Global, name string) (reflect.Value, error) {
	t := reflect.TypeOf(c).Elem()
	for i := 0; i < t.NumField(); i++ {
		if strings.HasPrefix(t.Field(i).Tag.Get("yaml"), name) {
			fieldValue := reflect.ValueOf(c).Elem().FieldByName(t.Field(i).Name)
			return fieldValue, nil
		}
	}
	return reflect.Value{}, fmt.Errorf("field not found on global config: %v", name)
}
