// Copyright 2022 schukai GmbH
// SPDX-License-Identifier: AGPL-3.0

package configuration

import (
	"io/fs"
	"os"
	"path"
	"path/filepath"

	"golang.org/x/exp/slices"
)

type files struct {
	path   string
	format Format
}

type fileBackend struct {
	directories []string
	files       []files
	format      Format
	name        string
	fs          fs.FS
}

func (s *Settings[c]) HasFile(file string) bool {
	for _, f := range s.files.files {
		if f.path == file {
			return true
		}
	}
	return false
}

// AddFile adds a file to the list of files to import
func (s *Settings[C]) AddFile(file string, format ...Format) *Settings[C] {

	var f Format

	errorCount := len(s.errors)
	defer func() {
		if len(s.errors) > errorCount {
			s.notifyErrorHooks()
		}
	}()

	if format == nil || len(format) == 0 {
		f = RecogniseFormat
	} else if format != nil && len(format) == 1 {
		f = format[0]
	} else {
		panic("too many arguments")
	}

	if f == RecogniseFormat {
		ext := filepath.Ext(file)
		switch ext {
		case ".yaml":
			f = Yaml
		case ".json":
			f = Json
		case ".toml":
			f = Toml
		case ".properties":
			f = Properties
		default:
			if ext != "" {
				s.errors = append(s.errors, newUnknownFileExtensionError(ext, file))
				return s
			}
			f = s.files.format
		}
	}

	s.files.files = append(s.files.files, files{file, f})
	return s
}

func (s *Settings[C]) RemoveFile(file string) *Settings[C] {

	for i, f := range s.files.files {
		if f.path == file {
			s.files.files = append(s.files.files[:i], s.files.files[i+1:]...)
			return s
		}
	}

	return s
}

func initFileBackend(files *fileBackend) {
	files.format = Yaml
	files.name = fileName
	files.fs = fs.FS(internalFS{})
}

// Directories returns the list of directories to search for configuration files
func (s *Settings[C]) Directories() []string {
	return s.files.directories
}

// AddDirectory adds a directory to the list of directories to search for configuration files
func (s *Settings[C]) AddDirectory(d string) *Settings[C] {
	s.Lock()
	defer s.Unlock()
	s.files.directories = append(s.files.directories, d)
	s.sanitizeDirectories()
	return s
}

func (s *Settings[C]) sanitizeDirectories() {

	errorCount := len(s.errors)
	defer func() {
		if len(s.errors) > errorCount {
			s.notifyErrorHooks()
		}
	}()

	wd, err := os.Getwd()
	if err != nil {
		s.errors = append(s.errors, err)
	}

	visited := make(map[string]bool, 0)
	for i := 0; i < len(s.files.directories); i++ {
		d := s.files.directories[i]

		if !path.IsAbs(d) {
			d = path.Join(wd, d)
		}

		if visited[d] == true {
			s.errors = append(s.errors, newPathAlreadyExistsError(d))
		} else {
			visited[d] = true
		}

	}

	// check last entry
	d := s.files.directories[len(s.files.directories)-1]
	if _, err := os.Stat(d); os.IsNotExist(err) {
		s.errors = append(s.errors, newPathDoesNotExistError(d))
	}

}

// SetDirectories sets the list of directories to search for configuration files
func (s *Settings[C]) SetDirectories(d []string) *Settings[C] {
	s.Lock()
	defer s.Unlock()

	s.files.directories = d
	s.sanitizeDirectories()
	return s
}

// AddWorkingDirectory adds the current working directory to the list of directories to search for configuration files
func (s *Settings[C]) AddWorkingDirectory() *Settings[C] {
	s.Lock()
	defer s.Unlock()

	errorCount := len(s.errors)
	defer func() {
		if len(s.errors) > errorCount {
			s.notifyErrorHooks()
		}
	}()

	current, err := os.Getwd()
	if err != nil {
		s.errors = append(s.errors, err)

		return s
	}
	s.files.directories = append(s.files.directories, current)
	s.sanitizeDirectories()
	return s
}

// AddEtcDirectory adds the /etc directory to the list of directories to search for configuration files
func (s *Settings[C]) AddEtcDirectory() *Settings[C] {
	s.Lock()
	defer s.Unlock()

	errorCount := len(s.errors)
	defer func() {
		if len(s.errors) > errorCount {
			s.notifyErrorHooks()
		}
	}()

	if s.mnemonic == "" {
		s.errors = append(s.errors, MnemonicEmptyError)
		return s
	}

	p := "/etc"
	if _, err := os.Stat(p); os.IsNotExist(err) {
		s.errors = append(s.errors, newPathDoesNotExistError(p))
		return s
	}

	p = path.Join(p, s.mnemonic)

	s.files.directories = append(s.files.directories, p)
	s.sanitizeDirectories()
	return s
}

// AddUserConfigDirectory Add the user configuration directory to the configuration directory
// The mnemonic must be set for this function
func (s *Settings[C]) AddUserConfigDirectory() *Settings[C] {

	s.Lock()
	defer s.Unlock()

	errorCount := len(s.errors)
	defer func() {
		if len(s.errors) > errorCount {
			s.notifyErrorHooks()
		}
	}()

	if s.mnemonic == "" {
		s.errors = append(s.errors, MnemonicEmptyError)
		return s
	}

	current, err := os.UserConfigDir()
	if err != nil {
		s.errors = append(s.errors, err)
		return s
	}

	current = path.Join(current, s.mnemonic)
	s.files.directories = append(s.files.directories, current)
	s.sanitizeDirectories()
	return s
}

// SetDefaultDirectories Add the current working directory, the user configuration directory
// and the Unix etc directory to the configuration directory
func (s *Settings[C]) SetDefaultDirectories() *Settings[C] {
	s.AddWorkingDirectory().
		AddUserConfigDirectory().
		AddEtcDirectory()
	return s
}

func (s *Settings[C]) SetFileFormat(format Format) *Settings[C] {

	errorCount := len(s.errors)
	defer func() {
		if len(s.errors) > errorCount {
			s.notifyErrorHooks()
		}
	}()

	if slices.Contains(availableFormats, format) {
		s.files.format = format
	} else {
		s.errors = append(s.errors, newFormatNotSupportedError(format))
	}

	return s
}

// SetFileName sets the name of the configuration file
func (s *Settings[C]) SetFileName(name string) *Settings[C] {

	errorCount := len(s.errors)
	defer func() {
		if len(s.errors) > errorCount {
			s.notifyErrorHooks()
		}
	}()

	if name == "" {
		s.errors = append(s.errors, FileNameEmptyError)
	} else {
		s.files.name = name
	}

	return s
}

func (s *Settings[C]) SetFilesystem(f fs.FS) *Settings[C] {
	s.files.fs = f
	return s
}