// Copyright 2020 The OPA Authors.  All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.

// Package config implements helper functions to parse OPA's configuration.
package config

import (
	"encoding/json"
	"fmt"
	"os"
	"regexp"
	"strings"

	"github.com/open-policy-agent/opa/internal/strvals"
	"github.com/open-policy-agent/opa/internal/yaml"
	"github.com/open-policy-agent/opa/v1/keys"
	"github.com/open-policy-agent/opa/v1/logging"
	"github.com/open-policy-agent/opa/v1/plugins/rest"
	"github.com/open-policy-agent/opa/v1/tracing"
	"github.com/open-policy-agent/opa/v1/util"
)

// ServiceOptions stores the options passed to ParseServicesConfig
type ServiceOptions struct {
	Raw                   json.RawMessage
	AuthPlugin            rest.AuthPluginLookupFunc
	Keys                  map[string]*keys.Config
	Logger                logging.Logger
	DistributedTacingOpts tracing.Options
	MinTLSVersion         uint16
	CipherSuites          *[]uint16
}

// ParseServicesConfig returns a set of named service clients. The service
// clients can be specified either as an array or as a map. Some systems (e.g.,
// Helm) do not have proper support for configuration values nested under
// arrays, so just support both here.
func ParseServicesConfig(opts ServiceOptions) (map[string]rest.Client, error) {

	services := map[string]rest.Client{}

	var arr []json.RawMessage
	var obj map[string]json.RawMessage
	clientOpts := []func(*rest.Client){
		rest.AuthPluginLookup(opts.AuthPlugin),
		rest.Logger(opts.Logger),
		rest.DistributedTracingOpts(opts.DistributedTacingOpts),
		rest.MinTLSVersion(opts.MinTLSVersion),
		rest.CipherSuites(opts.CipherSuites)}

	if err := util.Unmarshal(opts.Raw, &arr); err == nil {
		for _, s := range arr {
			client, err := rest.New(s, opts.Keys, clientOpts...)
			if err != nil {
				return nil, err
			}
			services[client.Service()] = client
		}
	} else if util.Unmarshal(opts.Raw, &obj) == nil {
		for k := range obj {
			client, err := rest.New(obj[k], opts.Keys, append(clientOpts, rest.Name(k))...)
			if err != nil {
				return nil, err
			}
			services[client.Service()] = client
		}
	} else {
		// Return error from array decode as that is the default format.
		return nil, err
	}

	return services, nil
}

// Load implements configuration file loading. The supplied config file will be
// read from disk (if specified) and overrides will be applied. If no config file is
// specified, the overrides can still be applied to an empty config.
func Load(configFile string, overrides []string, overrideFiles []string) ([]byte, error) {
	baseConf := map[string]any{}

	// User specified config file
	if configFile != "" {
		var bytes []byte
		var err error
		bytes, err = os.ReadFile(configFile)
		if err != nil {
			return nil, err
		}

		processedConf := subEnvVars(string(bytes))

		if err := yaml.Unmarshal([]byte(processedConf), &baseConf); err != nil {
			return nil, fmt.Errorf("failed to parse %s: %s", configFile, err)
		}
	}

	overrideConf := map[string]any{}

	// User specified a config override via --set
	for _, override := range overrides {
		processedOverride := subEnvVars(override)
		if err := strvals.ParseInto(processedOverride, overrideConf); err != nil {
			return nil, fmt.Errorf("failed parsing --set data: %s", err)
		}
	}

	// User specified a config override value via --set-file
	for _, override := range overrideFiles {
		reader := func(rs []rune) (any, error) {
			bytes, err := os.ReadFile(string(rs))
			value := strings.TrimSpace(string(bytes))
			return value, err
		}
		if err := strvals.ParseIntoFile(override, overrideConf, reader); err != nil {
			return nil, fmt.Errorf("failed parsing --set-file data: %s", err)
		}
	}

	// Merge together base config file and overrides, prefer the overrides
	conf := mergeValues(baseConf, overrideConf)

	// Take the patched config and marshal back to YAML
	return yaml.Marshal(conf)
}

// regex looking for ${...} notation strings
var envRegex = regexp.MustCompile(`(?U:\${.*})`)

// SubEnvVars will look for any environment variables in the passed in string
// with the syntax of ${VAR_NAME} and replace that string with ENV[VAR_NAME]
func SubEnvVars(s string) string {
	return subEnvVars(s)
}

func subEnvVars(s string) string {
	updatedConfig := envRegex.ReplaceAllStringFunc(s, func(s string) string {
		// Trim off the '${' and '}'
		if len(s) <= 3 {
			// This should never happen..
			return ""
		}
		varName := s[2 : len(s)-1]

		// Lookup the variable in the environment. We do not
		// play by bash rules: if its undefined we'll keep it
		// as-is, it could be replaced somewhere down the line.
		// If it's set to "", we'll return that.
		if lu, ok := os.LookupEnv(varName); ok {
			return lu
		}
		return s
	})

	return updatedConfig
}

// mergeValues will merge source and destination map, preferring values from the source map
func mergeValues(dest map[string]any, src map[string]any) map[string]any {
	for k, v := range src {
		// If the key doesn't exist already, then just set the key to that value
		if _, exists := dest[k]; !exists {
			dest[k] = v
			continue
		}
		nextMap, ok := v.(map[string]any)
		// If it isn't another map, overwrite the value
		if !ok {
			dest[k] = v
			continue
		}
		// Edge case: If the key exists in the destination, but isn't a map
		destMap, isMap := dest[k].(map[string]any)
		// If the source map has a map for this key, prefer it
		if !isMap {
			dest[k] = v
			continue
		}
		// If we got to this point, it is a map in both, so merge them
		dest[k] = mergeValues(destMap, nextMap)
	}
	return dest
}
