// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only

package cmd

import (
	"fmt"
	"io"
	"mime"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"reflect"
	"sort"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/go-playground/validator/v10"
	"github.com/go-viper/mapstructure/v2"

	"akvorado/common/helpers/yaml"

	"akvorado/common/helpers"
)

// ConfigRelatedOptions are command-line options related to handling a
// configuration file.
type ConfigRelatedOptions struct {
	Path       string
	Dump       bool
	BeforeDump func(mapstructure.Metadata)
}

// Parse parses the configuration file (if present) and the environment
// variables into the provided configuration. It returns the paths to watch if
// we want to detect configuration changes.
func (c ConfigRelatedOptions) Parse(out io.Writer, component string, config any) ([]string, error) {
	var rawConfig gin.H
	var paths []string
	if cfgFile := c.Path; cfgFile != "" {
		if strings.HasPrefix(cfgFile, "http://") || strings.HasPrefix(cfgFile, "https://") {
			u, err := url.Parse(cfgFile)
			if err != nil {
				return nil, fmt.Errorf("cannot parse configuration URL: %w", err)
			}
			if u.Path == "" {
				u.Path = fmt.Sprintf("/api/v0/orchestrator/configuration/%s", component)
			}
			if u.Fragment != "" {
				u.Path = fmt.Sprintf("%s/%s", u.Path, u.Fragment)
			}
			resp, err := http.Get(u.String())
			if err != nil {
				return nil, fmt.Errorf("unable to fetch configuration file: %w", err)
			}
			defer resp.Body.Close()
			contentType := resp.Header.Get("Content-Type")
			mediaType, _, err := mime.ParseMediaType(contentType)
			if (mediaType != "application/x-yaml" && mediaType != "application/yaml") || err != nil {
				return nil, fmt.Errorf("received configuration file is not YAML (%s)", contentType)
			}
			input, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, fmt.Errorf("unable to read configuration file: %w", err)
			}
			if err := yaml.Unmarshal(input, &rawConfig); err != nil {
				return nil, fmt.Errorf("unable to parse YAML configuration file: %w", err)
			}
		} else {
			cfgFile, err := filepath.EvalSymlinks(cfgFile)
			if err != nil {
				return nil, fmt.Errorf("cannot follow symlink: %w", err)
			}
			dirname, filename := filepath.Split(cfgFile)
			if dirname == "" {
				dirname = "."
			}
			paths, err = yaml.UnmarshalWithInclude(os.DirFS(dirname), filename, &rawConfig)
			for i := range paths {
				paths[i] = filepath.Clean(filepath.Join(dirname, paths[i]))
			}
			if err != nil {
				return nil, fmt.Errorf("unable to parse YAML configuration file: %w", err)
			}
		}
	}

	// Parse provided configuration
	defaultHook, disableDefaultHook := DefaultHook()
	zeroSliceHook, disableZeroSliceHook := ZeroSliceHook()
	var metadata mapstructure.Metadata
	decoderConfig := helpers.GetMapStructureDecoderConfig(&config, defaultHook, zeroSliceHook)
	decoderConfig.ErrorUnused = false
	decoderConfig.Metadata = &metadata
	decoder, err := mapstructure.NewDecoder(decoderConfig)
	if err != nil {
		return nil, fmt.Errorf("unable to create configuration decoder: %w", err)
	}
	if err := decoder.Decode(rawConfig); err != nil {
		return nil, fmt.Errorf("unable to parse configuration: %w", err)
	}
	disableDefaultHook()
	disableZeroSliceHook()

	// Override with environment variables
	for _, keyval := range os.Environ() {
		kv := strings.SplitN(keyval, "=", 2)
		if len(kv) != 2 {
			continue
		}
		kk := strings.Split(kv[0], "_")
		if len(kk) < 4 || kk[0] != "AKVORADO" || kk[1] != "CFG" || kk[2] != strings.ReplaceAll(strings.ToUpper(component), "-", "") {
			continue
		}
		// From AKVORADO_CFG_CMP_SQUID_PURPLE_QUIRK=47, we
		// build a map "squid -> purple -> quirk ->
		// 47". From AKVORADO_CFG_CMP_SQUID_3_PURPLE=47, we
		// build "squid[3] -> purple -> 47"
		var rawConfig any
		rawConfig = kv[1]
		for i := len(kk) - 1; i > 2; i-- {
			if index, err := strconv.Atoi(kk[i]); err == nil {
				newRawConfig := make([]any, index+1)
				newRawConfig[index] = rawConfig
				rawConfig = newRawConfig
			} else {
				rawConfig = gin.H{
					kk[i]: rawConfig,
				}
			}
		}
		if err := decoder.Decode(rawConfig); err != nil {
			return nil, fmt.Errorf("unable to parse override %q: %w", kv[0], err)
		}
	}

	// Check for unused keys
	invalidKeys := []string{}
	for _, key := range metadata.Unused {
		if !strings.HasPrefix(key, ".") && !strings.Contains(key, "..") {
			invalidKeys = append(invalidKeys, fmt.Sprintf("invalid key %q", key))
		}
	}
	sort.Strings(invalidKeys)
	if len(invalidKeys) > 0 {
		return nil, fmt.Errorf("invalid configuration:\n%s", strings.Join(invalidKeys, "\n"))
	}

	// Validate and dump configuration if requested
	if c.BeforeDump != nil {
		c.BeforeDump(metadata)
	}
	if err := helpers.Validate.Struct(config); err != nil {
		switch verr := err.(type) {
		case validator.ValidationErrors:
			return nil, fmt.Errorf("invalid configuration:\n%w", verr)
		default:
			return nil, fmt.Errorf("unexpected internal error: %w", verr)
		}
	}
	if c.Dump {
		output, err := yaml.Marshal(config)
		if err != nil {
			return nil, fmt.Errorf("unable to dump configuration: %w", err)
		}
		out.Write([]byte("---\n"))
		out.Write(output)
		out.Write([]byte("\n"))
	}

	return paths, nil
}

// DefaultHook will reset the destination value to its default using
// the Reset() method if present.
func DefaultHook() (mapstructure.DecodeHookFunc, func()) {
	disabled := false
	hook := func(from, to reflect.Value) (any, error) {
		if disabled {
			return from.Interface(), nil
		}
		if to.Kind() == reflect.Ptr {
			// We already have a pointer
			method, ok := to.Type().MethodByName("Reset")
			if !ok {
				// We may have a pointer to a pointer when totally empty.
				if !to.IsNil() {
					to = to.Elem()
					method, ok = to.Type().MethodByName("Reset")
				}
				if !ok {
					return from.Interface(), nil
				}
			}
			if to.IsNil() {
				newV := reflect.New(to.Type().Elem())
				method.Func.Call([]reflect.Value{newV})
				to.Set(newV)
				return from.Interface(), nil
			}
			method.Func.Call([]reflect.Value{to})
			return from.Interface(), nil
		}
		// Not a pointer, let's check if we take a pointer
		method, ok := reflect.PointerTo(to.Type()).MethodByName("Reset")
		if !ok {
			return from.Interface(), nil
		}
		method.Func.Call([]reflect.Value{to.Addr()})

		// Resume decoding
		return from.Interface(), nil
	}
	disable := func() {
		disabled = true
	}
	return hook, disable
}

// ZeroSliceHook clear a list got as a default value if we get a
// non-nil slice. Like DefaultHook, it can be disabled.
func ZeroSliceHook() (mapstructure.DecodeHookFunc, func()) {
	disabled := false
	hook := func(from, to reflect.Value) (any, error) {
		if disabled {
			return from.Interface(), nil
		}
		// Do we try to map a slice to a slice? If yes, the source slice should not be nil.
		if from.Kind() != reflect.Slice || to.Kind() != reflect.Slice || from.IsNil() || to.IsNil() {
			return from.Interface(), nil
		}

		// Zero out the destination slice.
		to.SetLen(0)

		// Resume decoding
		return from.Interface(), nil
	}
	disable := func() {
		disabled = true
	}
	return hook, disable
}
