// Copyright 2025 The Hugo Authors. All rights reserved.
//
// 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 config

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

	"github.com/gohugoio/hugo/common/herrors"
	"github.com/gohugoio/hugo/common/hmaps"
	"github.com/gohugoio/hugo/parser"

	"github.com/gohugoio/hugo/common/paths"

	"github.com/gohugoio/hugo/parser/metadecoders"
	"github.com/spf13/afero"
)

var (
	// See issue #8979 for context.
	// Hugo has always used config.toml etc. as the default config file name.
	// But hugo.toml is a more descriptive name, but we need to check for both.
	DefaultConfigNames = []string{"hugo", "config"}

	DefaultConfigNamesSet = make(map[string]bool)

	ValidConfigFileExtensions                    = []string{"toml", "yaml", "yml", "json"}
	validConfigFileExtensionsMap map[string]bool = make(map[string]bool)
)

func init() {
	for _, name := range DefaultConfigNames {
		DefaultConfigNamesSet[name] = true
	}

	for _, ext := range ValidConfigFileExtensions {
		validConfigFileExtensionsMap[ext] = true
	}
}

// IsValidConfigFilename returns whether filename is one of the supported
// config formats in Hugo.
func IsValidConfigFilename(filename string) bool {
	ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
	return validConfigFileExtensionsMap[ext]
}

// FromTOMLConfigString creates a config from the given TOML config. This is useful in tests.
func FromTOMLConfigString(config string) Provider {
	cfg, err := FromConfigString(config, "toml")
	if err != nil {
		panic(err)
	}
	return cfg
}

// FromMapToTOMLString converts the given map to a TOML string. This is useful in tests.
func FromMapToTOMLString(v map[string]any) string {
	var sb strings.Builder
	err := parser.InterfaceToConfig(v, metadecoders.TOML, &sb)
	if err != nil {
		panic(err)
	}
	return sb.String()
}

// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests.
func FromConfigString(config, configType string) (Provider, error) {
	m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config))
	if err != nil {
		return nil, err
	}
	return NewFrom(m), nil
}

// FromFile loads the configuration from the given filename.
func FromFile(fs afero.Fs, filename string) (Provider, error) {
	m, err := loadConfigFromFile(fs, filename)
	if err != nil {
		fe := herrors.UnwrapFileError(err)
		if fe != nil {
			pos := fe.Position()
			pos.Filename = filename
			fe.UpdatePosition(pos)
			return nil, err
		}
		return nil, herrors.NewFileErrorFromFile(err, filename, fs, nil)
	}
	return NewFrom(m), nil
}

// FromFileToMap is the same as FromFile, but it returns the config values
// as a simple map.
func FromFileToMap(fs afero.Fs, filename string) (map[string]any, error) {
	return loadConfigFromFile(fs, filename)
}

func readConfig(format metadecoders.Format, data []byte) (map[string]any, error) {
	m, err := metadecoders.Default.UnmarshalToMap(data, format)
	if err != nil {
		return nil, err
	}

	RenameKeys(m)

	return m, nil
}

func loadConfigFromFile(fs afero.Fs, filename string) (map[string]any, error) {
	m, err := metadecoders.Default.UnmarshalFileToMap(fs, filename)
	if err != nil {
		return nil, err
	}
	RenameKeys(m)
	return m, nil
}

func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provider, []string, error) {
	defaultConfigDir := filepath.Join(configDir, "_default")
	environmentConfigDir := filepath.Join(configDir, environment)
	cfg := New()

	var configDirs []string
	// Merge from least to most specific.
	for _, dir := range []string{defaultConfigDir, environmentConfigDir} {
		if _, err := sourceFs.Stat(dir); err == nil {
			configDirs = append(configDirs, dir)
		}
	}

	if len(configDirs) == 0 {
		return nil, nil, nil
	}

	// Keep track of these so we can watch them for changes.
	var dirnames []string

	for _, configDir := range configDirs {
		err := afero.Walk(sourceFs, configDir, func(path string, fi os.FileInfo, err error) error {
			if fi == nil || err != nil {
				return nil
			}

			if fi.IsDir() {
				dirnames = append(dirnames, path)
				return nil
			}

			if !IsValidConfigFilename(path) {
				return nil
			}

			name := paths.Filename(filepath.Base(path))

			item, err := metadecoders.Default.UnmarshalFileToMap(sourceFs, path)
			if err != nil {
				// This will be used in error reporting, use the most specific value.
				dirnames = []string{path}
				return fmt.Errorf("failed to unmarshal config for path %q: %w", path, err)
			}

			var keyPath []string
			var unwrapKey string
			if !DefaultConfigNamesSet[name] {
				// Can be params.jp, menus.en etc.
				name, lang := paths.FileAndExtNoDelimiter(name)
				unwrapKey = name

				keyPath = []string{name}

				if lang != "" {
					keyPath = []string{"languages", lang}
					switch name {
					case "menu", "menus":
						keyPath = append(keyPath, "menus")
					case "params":
						keyPath = append(keyPath, "params")
					}
				}
			}

			// TOML/YAML can't represent a headless top-level array, so allow a
			// file to wrap its content under a single top-level key matching
			// the basename (e.g. cascade.yaml with `cascade: [...]`).
			var itemValue any = item
			if unwrapKey != "" && len(item) == 1 {
				if inner, ok := item[unwrapKey]; ok {
					itemValue = inner
				}
			}

			root := item
			if len(keyPath) > 0 {
				root = make(map[string]any)
				m := root
				for i, key := range keyPath {
					if i >= len(keyPath)-1 {
						m[key] = itemValue
					} else {
						nm := make(map[string]any)
						m[key] = nm
						m = nm
					}
				}
			}

			// Migrate menu => menus etc.
			RenameKeys(root)

			// Set will overwrite keys with the same name, recursively.
			cfg.Set("", root)

			return nil
		})
		if err != nil {
			return nil, dirnames, err
		}

	}

	return cfg, dirnames, nil
}

var keyAliases hmaps.KeyRenamer

func init() {
	var err error
	keyAliases, err = hmaps.NewKeyRenamer(
		// Before 0.53 we used singular for "menu".
		"{menu,languages/*/menu}", "menus",
	)
	if err != nil {
		panic(err)
	}
}

// RenameKeys renames config keys in m recursively according to a global Hugo
// alias definition.
func RenameKeys(m map[string]any) {
	keyAliases.Rename(m)
}
