package configuration

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

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

	"github.com/authelia/authelia/v4/internal/configuration/schema"
)

// NewFileSource returns a configuration.Source configured to load from a specified path. If there is an issue
// accessing this path it also returns an error.
func NewFileSource(path string) (source *FileSource) {
	return &FileSource{
		koanf:     koanf.New(constDelimiter),
		provider:  FilteredFileProvider(path),
		providers: make(map[string]*FilteredFile),
		path:      path,
	}
}

// NewFilteredFileSource returns a configuration.Source configured to load from a specified path. If there is
// an issue accessing this path it also returns an error.
func NewFilteredFileSource(path string, filters ...BytesFilter) (source *FileSource) {
	return &FileSource{
		koanf:     koanf.New(constDelimiter),
		provider:  FilteredFileProvider(path, filters...),
		providers: make(map[string]*FilteredFile),
		path:      path,
		filters:   filters,
	}
}

// NewFileSources returns a slice of configuration.Source configured to load from specified files.
func NewFileSources(paths []string) (sources []*FileSource) {
	for _, path := range paths {
		source := NewFileSource(path)

		sources = append(sources, source)
	}

	return sources
}

// NewFilteredFileSources returns a slice of configuration.Source configured to load from specified files.
func NewFilteredFileSources(paths []string, filters []BytesFilter) (sources []*FileSource) {
	for _, path := range paths {
		source := NewFilteredFileSource(path, filters...)

		sources = append(sources, source)
	}

	return sources
}

// Name of the Source.
func (s *FileSource) Name() (name string) {
	return fmt.Sprintf("file path(%s)", s.path)
}

// Merge the FileSource koanf.Koanf into the provided one.
func (s *FileSource) Merge(ko *koanf.Koanf, _ *schema.StructValidator) (err error) {
	return ko.Merge(s.koanf)
}

// Load the Source into the FileSource koanf.Koanf.
func (s *FileSource) Load(val *schema.StructValidator) (err error) {
	if s.path == "" {
		return errors.New("invalid file path source configuration")
	}

	var info os.FileInfo

	if info, err = os.Stat(s.path); err != nil {
		return err
	}

	if info.IsDir() {
		return s.loadDir(val)
	}

	return s.koanf.Load(s.provider, yaml.Parser())
}

func (s *FileSource) loadDir(_ *schema.StructValidator) (err error) {
	var entries []os.DirEntry

	if entries, err = os.ReadDir(s.path); err != nil {
		return err
	}

	var (
		provider *FilteredFile
		ok       bool
	)

	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}

		name := entry.Name()

		file := filepath.Join(s.path, name)

		switch ext := filepath.Ext(name); ext {
		case extYML, extYAML:
			if provider, ok = s.providers[file]; !ok {
				provider = FilteredFileProvider(file, s.filters...)

				s.providers[file] = provider
			}

			if err = s.koanf.Load(provider, yaml.Parser()); err != nil {
				return err
			}
		}
	}

	return nil
}

// ReadFiles reads all the files associated with this FileSource.
func (s *FileSource) ReadFiles() (files []*File, err error) {
	if s.path == "" {
		return nil, errors.New("invalid file path source configuration")
	}

	var info os.FileInfo

	if info, err = os.Stat(s.path); err != nil {
		return nil, err
	}

	if info.IsDir() {
		return s.readFilesDirectory(s.path)
	}

	return s.readFilesFile(s.path)
}

func (s *FileSource) readFilesFile(path string) (files []*File, err error) {
	var file *File

	if file, err = s.readFile(path); err != nil {
		return nil, err
	}

	return []*File{file}, nil
}

func (s *FileSource) readFile(path string) (file *File, err error) {
	file = &File{
		Path: path,
	}

	if file.Data, err = FilteredFileProvider(path, s.filters...).ReadBytes(); err != nil {
		return nil, err
	}

	return file, err
}

func (s *FileSource) readFilesDirectory(path string) (files []*File, err error) {
	var entries []os.DirEntry

	if entries, err = os.ReadDir(path); err != nil {
		return nil, err
	}

	var file *File

	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}

		name := entry.Name()

		switch ext := filepath.Ext(name); ext {
		case extYML, extYAML:
			if file, err = s.readFile(filepath.Join(s.path, name)); err != nil {
				return nil, err
			}

			files = append(files, file)
		}
	}

	return files, nil
}

func (s *FileSource) GetBytesFilterNames() (names []string) {
	names = make([]string, len(s.filters))

	for i, filter := range s.filters {
		names[i] = filter.Name()
	}

	return names
}

// NewBytesSource returns a configuration.Source configured to load from a specified bytes.
func NewBytesSource(data []byte) (source *BytesSource) {
	return &BytesSource{
		koanf: koanf.New(constDelimiter),
		data:  data,
	}
}

// Name of the Source.
func (b *BytesSource) Name() (name string) {
	return fmt.Sprintf("bytes data(%s)", b.data)
}

// Merge the FileSource koanf.Koanf into the provided one.
func (b *BytesSource) Merge(ko *koanf.Koanf, _ *schema.StructValidator) (err error) {
	return ko.Merge(b.koanf)
}

// Load the Source into the FileSource koanf.Koanf.
func (b *BytesSource) Load(val *schema.StructValidator) (err error) {
	if b.data == nil {
		return nil
	}

	return b.koanf.Load(b, yaml.Parser())
}

// ReadBytes reads the contents of a file on disk, passes it through any configured filters, and returns the bytes.
func (b *BytesSource) ReadBytes() (data []byte, err error) {
	if len(b.filters) == 0 {
		return b.data, nil
	}

	data = make([]byte, len(b.data))

	copy(data, b.data)

	for _, filter := range b.filters {
		if data, err = filter.Filter(data); err != nil {
			return nil, err
		}
	}

	return data, nil
}

// Read is not supported by the filtered file koanf.Provider.
func (b *BytesSource) Read() (map[string]any, error) {
	return nil, errors.New("filtered bytes provider does not support this method")
}

// NewEnvironmentSource returns a Source configured to load from environment variables.
func NewEnvironmentSource(prefix, delimiter string) (source *EnvironmentSource) {
	return &EnvironmentSource{
		koanf:     koanf.New(constDelimiter),
		prefix:    prefix,
		delimiter: delimiter,
	}
}

// Name of the Source.
func (s *EnvironmentSource) Name() (name string) {
	return "environment"
}

// Merge the EnvironmentSource koanf.Koanf into the provided one.
func (s *EnvironmentSource) Merge(ko *koanf.Koanf, _ *schema.StructValidator) (err error) {
	return ko.Merge(s.koanf)
}

// Load the Source into the EnvironmentSource koanf.Koanf.
func (s *EnvironmentSource) Load(_ *schema.StructValidator) (err error) {
	keyMap, ignoredKeys := getEnvConfigMap(schema.Keys, s.prefix, s.delimiter, deprecations, deprecationsMKM)

	return s.koanf.Load(env.Provider(constDelimiter, env.Opt{Prefix: s.prefix, TransformFunc: koanfEnvironmentCallback(keyMap, ignoredKeys, s.prefix, s.delimiter)}), nil)
}

// NewSecretsSource returns a Source configured to load from secrets.
func NewSecretsSource(prefix, delimiter string) (source *SecretsSource) {
	return &SecretsSource{
		koanf:     koanf.New(constDelimiter),
		prefix:    prefix,
		delimiter: delimiter,
	}
}

// Name of the Source.
func (s *SecretsSource) Name() (name string) {
	return "secrets"
}

// Merge the SecretsSource koanf.Koanf into the provided one.
func (s *SecretsSource) Merge(ko *koanf.Koanf, val *schema.StructValidator) (err error) {
	for _, key := range s.koanf.Keys() {
		value, ok := ko.Get(key).(string)

		if ok && value != "" {
			val.Push(fmt.Errorf(errFmtSecretAlreadyDefined, key))
		}
	}

	return ko.Merge(s.koanf)
}

// Load the Source into the SecretsSource koanf.Koanf.
func (s *SecretsSource) Load(val *schema.StructValidator) (err error) {
	keyMap := getSecretConfigMap(schema.Keys, s.prefix, s.delimiter, deprecations)

	return s.koanf.Load(env.Provider(constDelimiter, env.Opt{Prefix: s.prefix, TransformFunc: koanfEnvironmentSecretsCallback(keyMap, val)}), nil)
}

// NewCommandLineSourceWithMapping creates a new command line configuration source with a map[string]string which converts
// flag names into other config key names. If includeValidKeys is true we also allow any flag with a name which matches
// the list of valid keys into the koanf.Koanf, otherwise everything not in the map is skipped. Unchanged flags are also
// skipped unless includeUnchangedKeys is set to true.
func NewCommandLineSourceWithMapping(flags *pflag.FlagSet, mapping map[string]string, includeValidKeys, includeUnchangedKeys bool) (source *CommandLineSource) {
	return &CommandLineSource{
		koanf:    koanf.New(constDelimiter),
		flags:    flags,
		callback: koanfCommandLineWithMappingCallback(mapping, includeValidKeys, includeUnchangedKeys),
	}
}

// Name of the Source.
func (s *CommandLineSource) Name() (name string) {
	return "command-line"
}

// Merge the CommandLineSource koanf.Koanf into the provided one.
func (s *CommandLineSource) Merge(ko *koanf.Koanf, val *schema.StructValidator) (err error) {
	return ko.Merge(s.koanf)
}

// Load the Source into the FileSource koanf.Koanf.
func (s *CommandLineSource) Load(_ *schema.StructValidator) (err error) {
	if s.callback != nil {
		return s.koanf.Load(posflag.ProviderWithFlag(s.flags, ".", s.koanf, s.callback), nil)
	}

	return s.koanf.Load(posflag.Provider(s.flags, ".", s.koanf), nil)
}

// NewMapSource returns a new map[string]any source.
func NewMapSource(m map[string]any) (source *MapSource) {
	return &MapSource{
		m:     m,
		koanf: koanf.New(constDelimiter),
	}
}

// Name of the Source.
func (s *MapSource) Name() (name string) {
	return "map"
}

// Merge the CommandLineSource koanf.Koanf into the provided one.
func (s *MapSource) Merge(ko *koanf.Koanf, val *schema.StructValidator) (err error) {
	return ko.Merge(s.koanf)
}

// Load the Source into the FileSource koanf.Koanf.
func (s *MapSource) Load(_ *schema.StructValidator) (err error) {
	return s.koanf.Load(confmap.Provider(s.m, constDelimiter), nil)
}

// NewDefaultSources returns a slice of Source configured to load from specified YAML files.
func NewDefaultSources(paths []string, prefix, delimiter string, additionalSources ...Source) (sources []Source) {
	sources = []Source{NewMapSource(defaults)}

	fileSources := NewFileSources(paths)
	for _, source := range fileSources {
		sources = append(sources, source)
	}

	sources = append(sources, NewEnvironmentSource(prefix, delimiter))
	sources = append(sources, NewSecretsSource(prefix, delimiter))

	if len(additionalSources) != 0 {
		sources = append(sources, additionalSources...)
	}

	return sources
}

// NewDefaultSourcesFiltered returns a slice of Source configured to load from specified YAML files.
func NewDefaultSourcesFiltered(paths []string, filters []BytesFilter, prefix, delimiter string, additionalSources ...Source) (sources []Source) {
	sources = []Source{NewMapSource(defaults)}

	fileSources := NewFilteredFileSources(paths, filters)
	for _, source := range fileSources {
		sources = append(sources, source)
	}

	sources = append(sources, NewEnvironmentSource(prefix, delimiter))
	sources = append(sources, NewSecretsSource(prefix, delimiter))

	if len(additionalSources) != 0 {
		sources = append(sources, additionalSources...)
	}

	return sources
}

// NewDefaultSourcesWithDefaults returns a slice of Source configured to load from specified YAML files with additional sources.
func NewDefaultSourcesWithDefaults(paths []string, filters []BytesFilter, prefix, delimiter string, defaultSources []Source, additionalSources ...Source) (sources []Source) {
	sources = []Source{NewMapSource(defaults)}

	if len(defaultSources) != 0 {
		sources = append(sources, defaultSources...)
	}

	if len(filters) == 0 {
		sources = append(sources, NewDefaultSources(paths, prefix, delimiter, additionalSources...)...)
	} else {
		sources = append(sources, NewDefaultSourcesFiltered(paths, filters, prefix, delimiter, additionalSources...)...)
	}

	return sources
}

// NewDefaultsSource is a base configuration which sets defaults, this is particularly useful at the present time for
// setting defaults that otherwise can't be set. In the future it can be used to generate documentation or be generated
// by jsonschema. It will also reduce some areas of the validation package.
func NewDefaultsSource() (source Source) {
	return NewMapSource(mapDefaults)
}
