// Copyright 2024 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 allconfig

import (
	"fmt"
	"sort"
	"strings"

	"github.com/gohugoio/hugo/cache/filecache"
	"github.com/gohugoio/hugo/langs"

	"github.com/gohugoio/hugo/cache/httpcache"
	"github.com/gohugoio/hugo/common/hmaps"
	"github.com/gohugoio/hugo/common/hstrings"
	"github.com/gohugoio/hugo/common/loggers"
	"github.com/gohugoio/hugo/common/types"
	"github.com/gohugoio/hugo/config"
	"github.com/gohugoio/hugo/config/privacy"
	"github.com/gohugoio/hugo/config/security"
	"github.com/gohugoio/hugo/config/services"
	"github.com/gohugoio/hugo/deploy/deployconfig"
	"github.com/gohugoio/hugo/hugolib/roles"
	"github.com/gohugoio/hugo/hugolib/segments"
	"github.com/gohugoio/hugo/hugolib/versions"
	"github.com/gohugoio/hugo/markup/markup_config"
	"github.com/gohugoio/hugo/media"
	"github.com/gohugoio/hugo/minifiers"
	"github.com/gohugoio/hugo/modules"

	"github.com/gohugoio/hugo/navigation"
	"github.com/gohugoio/hugo/output"
	"github.com/gohugoio/hugo/related"
	"github.com/gohugoio/hugo/resources/images"
	"github.com/gohugoio/hugo/resources/page"
	"github.com/gohugoio/hugo/resources/page/pagemeta"
	"github.com/mitchellh/mapstructure"
	"github.com/spf13/afero"
	"github.com/spf13/cast"
)

type decodeConfig struct {
	p      config.Provider
	c      *Config
	fs     afero.Fs
	bcfg   config.BaseConfig
	logger loggers.Logger
}

type decodeWeight struct {
	key                  string
	decode               func(decodeWeight, decodeConfig) error
	getCompiler          func(c *Config) configCompiler
	getInitializer       func(c *Config) configInitializer
	weight               int
	internalOrDeprecated bool // Hide it from the docs.
}

var allDecoderSetups = map[string]decodeWeight{
	"": {
		key:    "",
		weight: -100, // Always first.
		decode: func(d decodeWeight, p decodeConfig) error {
			if err := mapstructure.WeakDecode(p.p.Get(""), &p.c.RootConfig); err != nil {
				return err
			}

			// This need to match with the map keys, always lower case.
			p.c.RootConfig.DefaultContentLanguage = strings.ToLower(p.c.RootConfig.DefaultContentLanguage)
			p.c.RootConfig.DefaultContentRole = strings.ToLower(p.c.RootConfig.DefaultContentRole)
			p.c.RootConfig.DefaultContentVersion = strings.ToLower(p.c.RootConfig.DefaultContentVersion)

			return nil
		},
	},
	"imaging": {
		key: "imaging",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.Imaging, err = images.DecodeConfig(p.p.GetStringMap(d.key))
			return err
		},
	},
	"caches": {
		key: "caches",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.Caches, err = filecache.DecodeConfig(p.fs, p.bcfg, p.p.GetStringMap(d.key))
			if p.c.IgnoreCache {
				// Set MaxAge in all caches to 0.
				for k, cache := range p.c.Caches {
					cache.MaxAge = 0
					p.c.Caches[k] = cache
				}
			}
			return err
		},
	},
	"httpcache": {
		key: "httpcache",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.HTTPCache, err = httpcache.DecodeConfig(p.bcfg, p.p.GetStringMap(d.key))
			if p.c.IgnoreCache {
				p.c.HTTPCache.Cache.For.Excludes = []string{"**"}
				p.c.HTTPCache.Cache.For.Includes = []string{}
			}
			return err
		},
	},
	"build": {
		key: "build",
		decode: func(d decodeWeight, p decodeConfig) error {
			p.c.Build = config.DecodeBuildConfig(p.p)
			return nil
		},
		getCompiler: func(c *Config) configCompiler {
			return &c.Build
		},
	},
	"frontmatter": {
		key: "frontmatter",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.Frontmatter, err = pagemeta.DecodeFrontMatterConfig(p.p)
			return err
		},
	},
	"markup": {
		key: "markup",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.Markup, err = markup_config.Decode(p.p)
			return err
		},
	},
	"segments": {
		key: "segments",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.Segments, err = segments.DecodeSegments(p.p.GetStringMap(d.key), p.c.RenderSegments, p.logger)
			return err
		},
		getInitializer: func(c *Config) configInitializer {
			return c.Segments.Config
		},
	},
	"server": {
		key: "server",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.Server, err = config.DecodeServer(p.p)
			return err
		},
		getCompiler: func(c *Config) configCompiler {
			return &c.Server
		},
	},
	"minify": {
		key: "minify",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.Minify, err = minifiers.DecodeConfig(p.p.Get(d.key))
			return err
		},
	},
	"contenttypes": {
		key:    "contenttypes",
		weight: 100, // This needs to be decoded after media types.
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.ContentTypes, err = media.DecodeContentTypes(p.p.GetStringMap(d.key), p.c.MediaTypes.Config)
			return err
		},
	},
	"mediatypes": {
		key: "mediatypes",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.MediaTypes, err = media.DecodeTypes(p.p.GetStringMap(d.key))
			return err
		},
	},
	"outputs": {
		key: "outputs",
		decode: func(d decodeWeight, p decodeConfig) error {
			defaults := createDefaultOutputFormats(p.c.OutputFormats.Config)
			m := hmaps.CleanConfigStringMap(p.p.GetStringMap("outputs"))
			p.c.Outputs = make(map[string][]string)
			for k, v := range m {
				s := types.ToStringSlicePreserveString(v)
				for i, v := range s {
					s[i] = strings.ToLower(v)
				}
				p.c.Outputs[k] = s
			}
			// Apply defaults.
			for k, v := range defaults {
				if _, found := p.c.Outputs[k]; !found {
					p.c.Outputs[k] = v
				}
			}
			return nil
		},
	},
	"outputformats": {
		key: "outputformats",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.OutputFormats, err = output.DecodeConfig(p.c.MediaTypes.Config, p.p.Get(d.key))
			return err
		},
	},
	"languages": {
		key: "languages",
		decode: func(d decodeWeight, p decodeConfig) error {
			m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
			if len(m) == 1 {
				// In v0.112.4 we moved this to the language config, but it's very commmon for mono language sites to have this at the top level.
				var first hmaps.Params
				var ok bool
				for _, v := range m {
					first, ok = v.(hmaps.Params)
					if ok {
						break
					}
				}
				if first != nil {
					if _, found := first["languagecode"]; !found {
						first["languagecode"] = p.p.GetString("languagecode")
					}
				}
			}
			var (
				err                    error
				defaultContentLanguage string
			)
			p.c.Languages, defaultContentLanguage, err = langs.DecodeConfig(p.c.RootConfig.DefaultContentLanguage, p.c.RootConfig.DisableLanguages, m)
			if err != nil {
				return fmt.Errorf("failed to decode languages config: %w", err)
			}
			for k, v := range p.c.Languages.Config.LanguageConfigs {
				if v.Disabled {
					p.c.RootConfig.DisableLanguages = append(p.c.RootConfig.DisableLanguages, k)
				}
			}

			p.c.RootConfig.DisableLanguages = hstrings.UniqueStringsReuse(p.c.RootConfig.DisableLanguages)
			sort.Strings(p.c.RootConfig.DisableLanguages)
			p.c.RootConfig.DefaultContentLanguage = defaultContentLanguage
			return nil
		},
	},
	"versions": {
		key: "versions",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
			p.c.Versions, err = versions.DecodeConfig(p.c.RootConfig.DefaultContentVersion, m)
			return err
		},
	},
	"roles": {
		key: "roles",
		decode: func(d decodeWeight, p decodeConfig) error {
			var (
				err                error
				defaultContentRole string
			)
			m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
			p.c.Roles, defaultContentRole, err = roles.DecodeConfig(p.c.RootConfig.DefaultContentRole, m)
			p.c.RootConfig.DefaultContentRole = defaultContentRole

			return err
		},
	},

	"params": {
		key: "params",
		decode: func(d decodeWeight, p decodeConfig) error {
			p.c.Params = hmaps.CleanConfigStringMap(p.p.GetStringMap("params"))
			if p.c.Params == nil {
				p.c.Params = make(map[string]any)
			}

			// Before Hugo 0.112.0 this was configured via site Params.
			if mainSections, found := p.c.Params["mainsections"]; found {
				p.c.MainSections = types.ToStringSlicePreserveString(mainSections)
				if p.c.MainSections == nil {
					p.c.MainSections = []string{}
				}
			}

			return nil
		},
	},
	"module": {
		key: "module",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.Module, err = modules.DecodeConfig(p.logger.Logger(), p.p)
			return err
		},
	},
	"permalinks": {
		key: "permalinks",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.Permalinks, err = page.DecodePermalinksConfig(p.p.GetStringMap(d.key))
			return err
		},
	},
	"sitemap": {
		key: "sitemap",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			if p.p.IsSet(d.key) {
				p.c.Sitemap, err = config.DecodeSitemap(p.c.Sitemap, p.p.GetStringMap(d.key))
			}
			return err
		},
	},
	"taxonomies": {
		key: "taxonomies",
		decode: func(d decodeWeight, p decodeConfig) error {
			if p.p.IsSet(d.key) {
				m := hmaps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
				// Remove invalid entries (e.g. non-taxonomy keys placed inside [taxonomies] in TOML).
				for k, v := range m {
					if k == "" || v == "" {
						delete(m, k)
					}
				}
				p.c.Taxonomies = m
			}
			return nil
		},
	},
	"related": {
		key:    "related",
		weight: 100, // This needs to be decoded after taxonomies.
		decode: func(d decodeWeight, p decodeConfig) error {
			if p.p.IsSet(d.key) {
				var err error
				p.c.Related, err = related.DecodeConfig(p.p.GetParams(d.key))
				if err != nil {
					return fmt.Errorf("failed to decode related config: %w", err)
				}
			} else {
				p.c.Related = related.DefaultConfig
				if _, found := p.c.Taxonomies["tag"]; found {
					p.c.Related.Add(related.IndexConfig{Name: "tags", Weight: 80, Type: related.TypeBasic})
				}
			}
			return nil
		},
	},

	"cascade": {
		key: "cascade",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.Cascade, err = page.DecodeCascadeConfig(p.p.Get(d.key))
			return err
		},
		getInitializer: func(c *Config) configInitializer { return c.Cascade },
	},
	"menus": {
		key: "menus",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.Menus, err = navigation.DecodeConfig(p.p.Get(d.key))
			return err
		},
	},
	"page": {
		key: "page",
		decode: func(d decodeWeight, p decodeConfig) error {
			p.c.Page = config.PageConfig{
				NextPrevSortOrder:          "desc",
				NextPrevInSectionSortOrder: "desc",
			}
			if p.p.IsSet(d.key) {
				if err := mapstructure.WeakDecode(p.p.Get(d.key), &p.c.Page); err != nil {
					return err
				}
			}

			return nil
		},
		getCompiler: func(c *Config) configCompiler {
			return &c.Page
		},
	},
	"pagination": {
		key: "pagination",
		decode: func(d decodeWeight, p decodeConfig) error {
			p.c.Pagination = config.Pagination{
				PagerSize: 10,
				Path:      "page",
			}
			if p.p.IsSet(d.key) {
				if err := mapstructure.WeakDecode(p.p.Get(d.key), &p.c.Pagination); err != nil {
					return err
				}
			}

			return nil
		},
	},
	"privacy": {
		key: "privacy",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.Privacy, err = privacy.DecodeConfig(p.p)
			return err
		},
	},
	"security": {
		key: "security",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.Security, err = security.DecodeConfig(p.p)
			return err
		},
	},
	"services": {
		key: "services",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.Services, err = services.DecodeConfig(p.p)
			return err
		},
	},
	"deployment": {
		key: "deployment",
		decode: func(d decodeWeight, p decodeConfig) error {
			var err error
			p.c.Deployment, err = deployconfig.DecodeConfig(p.p)
			return err
		},
	},
	"author": {
		key: "author",
		decode: func(d decodeWeight, p decodeConfig) error {
			p.c.Author = hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
			return nil
		},
		internalOrDeprecated: true,
	},
	"social": {
		key: "social",
		decode: func(d decodeWeight, p decodeConfig) error {
			p.c.Social = hmaps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
			return nil
		},
		internalOrDeprecated: true,
	},
	"uglyurls": {
		key: "uglyurls",
		decode: func(d decodeWeight, p decodeConfig) error {
			v := p.p.Get(d.key)
			switch vv := v.(type) {
			case bool:
				p.c.UglyURLs = vv
			case string:
				p.c.UglyURLs = vv == "true"
			case hmaps.Params:
				p.c.UglyURLs = cast.ToStringMapBool(hmaps.CleanConfigStringMap(vv))
			default:
				p.c.UglyURLs = cast.ToStringMapBool(v)
			}
			return nil
		},
		internalOrDeprecated: true,
	},
	"internal": {
		key: "internal",
		decode: func(d decodeWeight, p decodeConfig) error {
			return mapstructure.WeakDecode(p.p.GetStringMap(d.key), &p.c.Internal)
		},
		internalOrDeprecated: true,
	},
}

func init() {
	for k, v := range allDecoderSetups {
		// Verify that k and v.key is all lower case.
		if k != strings.ToLower(k) {
			panic(fmt.Sprintf("key %q is not lower case", k))
		}
		if v.key != strings.ToLower(v.key) {
			panic(fmt.Sprintf("key %q is not lower case", v.key))
		}

		if k != v.key {
			panic(fmt.Sprintf("key %q is not the same as the map key %q", k, v.key))
		}
	}
}
