package config

import (
	"context"
	"fmt"
	"strconv"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
	"github.com/creasty/defaults"
	"github.com/go-playground/validator/v10"
	"github.com/imdario/mergo"
	"github.com/mitchellh/mapstructure"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/spf13/viper"
	ov "github.com/tidbcloud/config-service/oneconfig/viper"
	"go.uber.org/zap"
	"gopkg.in/yaml.v2"
)

// use defaults <- pulumi config (config file) <- remote config <- secrets
// defaults for tag default in the config struct.

// use a single instance of Validate, it caches struct info
var validate = validator.New()

const RemoteConfigPath = "oneconfig-serverless/config.yaml"

func NormalizeConfigDefaults(config *CentralConfig) error {
	if err := defaults.Set(config); err != nil {
		return err
	}
	if err := normalizeNewArchShareScope(config); err != nil {
		return err
	}
	if err := normalizeNewArchEnabledClusters(config); err != nil {
		return err
	}
	if len(config.CDCConfig.NewArch.NodeSelector) == 0 {
		config.CDCConfig.NewArch.NodeSelector = map[string]string{}
		for k, v := range DefaultNewArchNodeSelector {
			config.CDCConfig.NewArch.NodeSelector[k] = v
		}
	}
	if len(config.CDCConfig.NewArch.Tolerations) == 0 {
		config.CDCConfig.NewArch.Tolerations = append(
			[]NewArchToleration(nil),
			DefaultNewArchTolerations...,
		)
	}
	return nil
}

func normalizeNewArchEnabledClusters(config *CentralConfig) error {
	for i, clusterID := range config.CDCConfig.NewArch.EnabledClusters {
		if _, err := strconv.ParseUint(clusterID, 10, 64); err != nil {
			return fmt.Errorf("invalid cdc.newArch.enabledClusters[%d] %q: %w", i, clusterID, err)
		}
	}
	return nil
}

func normalizeNewArchShareScope(config *CentralConfig) error {
	if err := normalizeOneNewArchShareScope(&config.CDCConfig.NewArch.ShareScope); err != nil {
		return fmt.Errorf("invalid cdc.newArch.shareScope: %w", err)
	}
	for i := range config.CDCConfig.NewArch.ShareRules {
		if err := normalizeOneNewArchShareScope(&config.CDCConfig.NewArch.ShareRules[i].ShareScope); err != nil {
			return fmt.Errorf("invalid cdc.newArch.shareRules[%d].shareScope: %w", i, err)
		}
	}
	return nil
}

func normalizeOneNewArchShareScope(scope *string) error {
	switch *scope {
	case "", "global":
		*scope = "global"
	case "tenant", "cluster", "changefeed":
	default:
		return fmt.Errorf("invalid share scope %q", *scope)
	}
	return nil
}

func NormalizeConfigSetup(config *CentralConfig) error {
	settings := viper.AllSettings()
	if err := normalizeDottedNodeSelector(settings); err != nil {
		return errors.Annotate(err, "failed normalize cdc newArch nodeSelector")
	}

	decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
		TagName:          "mapstructure",
		Result:           config,
		WeaklyTypedInput: true,
		DecodeHook: mapstructure.ComposeDecodeHookFunc(
			mapstructure.StringToTimeDurationHookFunc(),
			mapstructure.StringToSliceHookFunc(","),
		),
	})
	if err != nil {
		return errors.Annotate(err, "failed create config decoder")
	}

	err = decoder.Decode(settings)
	if err != nil {
		return errors.Annotate(err, "failed unmarshal from viper")
	}

	// let's load some secret from aws and merge it
	if config.ServerConfig.SecretStore != nil {
		secretConfig, err := GetSecretFromAwsSecretManager(
			config.ServerConfig.SecretStore.SecretKey, config.ServerConfig.SecretStore.Region,
		)
		if err != nil {
			return errors.Annotate(err, "failed to load secret config from aws")
		}
		m := make(map[any]any)
		if err := yaml.Unmarshal([]byte(secretConfig), &m); err != nil {
			return errors.Annotate(err, "failed to unmarshal secret config")
		}

		var s CentralConfig

		if err := mapstructure.Decode(m, &s); err != nil {
			return errors.Annotate(err, "failed to mapsturcture secret config")
		}

		if err := mergo.Merge(config, s, mergo.WithOverride); err != nil {
			return errors.Annotate(err, "failed to merge secret config")
		}
	}
	return nil
}

func normalizeDottedNodeSelector(settings map[string]any) error {
	cdc, ok := settings["cdc"].(map[string]any)
	if !ok {
		return nil
	}
	newArch, ok := cdc["newarch"].(map[string]any)
	if !ok {
		return nil
	}
	nodeSelector, ok := newArch["nodeselector"].(map[string]any)
	if !ok {
		return nil
	}

	flattened := make(map[string]string, len(nodeSelector))
	if err := flattenStringMap("", nodeSelector, flattened); err != nil {
		return err
	}
	newArch["nodeselector"] = flattened
	return nil
}

func flattenStringMap(prefix string, raw map[string]any, flattened map[string]string) error {
	for key, value := range raw {
		fullKey := key
		if prefix != "" {
			fullKey = prefix + "." + key
		}

		switch typed := value.(type) {
		case string:
			flattened[fullKey] = typed
		case map[string]any:
			if err := flattenStringMap(fullKey, typed, flattened); err != nil {
				return err
			}
		default:
			return errors.Errorf("invalid string map value for %q: %T", fullKey, value)
		}
	}
	return nil
}

func NormalizeConfigValidate(config *CentralConfig) error {
	// validate the values
	if err := validate.Struct(config); err != nil {
		return errors.Annotate(err, "failed to validate the config values")
	}
	return nil
}

func NormalizeConfig(validate bool) error {
	config := &CentralConfig{}

	if err := NormalizeConfigDefaults(config); err != nil {
		return err
	}

	if err := NormalizeConfigSetup(config); err != nil {
		return err
	}

	if validate {
		if err := NormalizeConfigValidate(config); err != nil {
			return err
		}
	}
	globalConf = config
	return nil
}

// InitConfig parse the central configuration file and init the global config var
func InitConfig(path string, validate bool) error {
	viper.SetConfigFile(path)
	viper.AutomaticEnv() // for M2M env variables
	if err := viper.ReadInConfig(); err != nil {
		return err
	}

	if err := LoadRemote(); err != nil {
		log.Warn("load remote config failed", zap.Error(err))
	}

	return NormalizeConfig(validate)
}

func LoadRemote() error {
	if err := viper.AddRemoteProvider(ov.Provider, ov.Endpoint, RemoteConfigPath); err != nil {
		return errors.Annotate(err, "failed to load remote config")
	}

	if err := viper.ReadRemoteConfig(); err != nil {
		return errors.Annotate(err, "failed to load remote config")
	}

	return nil
}

func Global() *CentralConfig {
	return globalConf
}

// Set replaces the process-global CentralConfig. Tests use it to
// arrange a known config without going through the full
// InitConfig file-parse path. Production code goes through
// InitConfig and should not call Set.
func Set(c *CentralConfig) {
	globalConf = c
}

func GetSecretFromAwsSecretManager(secretName, region string) (string, error) {
	config, err := config.LoadDefaultConfig(
		context.Background(),
		config.WithRegion(region),
		config.WithRetryMaxAttempts(3),
	)
	if err != nil {
		return "", err
	}
	svc := secretsmanager.NewFromConfig(config)
	input := &secretsmanager.GetSecretValueInput{
		SecretId:     aws.String(secretName),
		VersionStage: aws.String("AWSCURRENT"), // VersionStage defaults to AWSCURRENT if unspecified
	}
	result, err := svc.GetSecretValue(context.Background(), input)
	if err != nil {
		return "", err
	}
	return *result.SecretString, nil
}
