package config

import (
	"context"
	"fmt"
	"net/netip"
	"strings"
	"time"

	awsconfig "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/go-sql-driver/mysql"
	"github.com/imdario/mergo"
	"github.com/mitchellh/mapstructure"
	"github.com/spf13/viper"
	"gopkg.in/yaml.v2"
	"k8s.io/utils/pointer"
)

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

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

// InitConfig parse the central configuration file and init the global config var
func InitConfig(path string) (*Config, error) {
	viper.SetConfigFile(path)
	viper.AutomaticEnv() // for M2M env variables

	if err := viper.ReadInConfig(); err != nil {
		return nil, err
	}

	c := &Config{}

	if err := defaults.Set(c); err != nil {
		return nil, err
	}

	if err := mergeMultipleConfigs(c, loadRemoteConfig); err != nil {
		return nil, err
	}

	if err := validate.Struct(c); err != nil {
		return nil, fmt.Errorf("failed to validate the config values: %w", err)
	}
	if err := validateResourcePoolNetworkConfig(c.ResourcePoolNetwork); err != nil {
		return nil, err
	}

	return c, nil
}

func validateResourcePoolNetworkConfig(config ResourcePoolNetworkConfig) error {
	awsMinPrefixLength := config.AWSMinPrefixLength
	awsMaxPrefixLength := config.AWSMaxPrefixLength
	if awsMinPrefixLength < 1 || awsMinPrefixLength > 32 {
		return fmt.Errorf(
			"resourcePoolNetwork.awsMinPrefixLength (%d) must be between 1 and 32",
			awsMinPrefixLength,
		)
	}
	if awsMaxPrefixLength < 1 || awsMaxPrefixLength > 32 {
		return fmt.Errorf(
			"resourcePoolNetwork.awsMaxPrefixLength (%d) must be between 1 and 32",
			awsMaxPrefixLength,
		)
	}
	if awsMinPrefixLength > awsMaxPrefixLength {
		return fmt.Errorf(
			"resourcePoolNetwork.awsMinPrefixLength (%d) must not exceed resourcePoolNetwork.awsMaxPrefixLength (%d)",
			awsMinPrefixLength,
			awsMaxPrefixLength,
		)
	}
	if config.AWSRecommendationStartPrefixLength != 0 &&
		(config.AWSRecommendationStartPrefixLength < awsMinPrefixLength ||
			config.AWSRecommendationStartPrefixLength > awsMaxPrefixLength) {
		return fmt.Errorf(
			"resourcePoolNetwork.awsRecommendationStartPrefixLength (%d) must be between "+
				"resourcePoolNetwork.awsMinPrefixLength (%d) and resourcePoolNetwork.awsMaxPrefixLength (%d)",
			config.AWSRecommendationStartPrefixLength,
			awsMinPrefixLength,
			awsMaxPrefixLength,
		)
	}

	privateRanges := []netip.Prefix{
		netip.MustParsePrefix("10.0.0.0/8"),
		netip.MustParsePrefix("172.16.0.0/12"),
		netip.MustParsePrefix("192.168.0.0/16"),
	}
	for _, configuredCIDR := range config.ReservedCIDRs {
		if configuredCIDR == "" || configuredCIDR != strings.TrimSpace(configuredCIDR) {
			return fmt.Errorf("resourcePoolNetwork.reservedCidrs contains an empty or whitespace-modified CIDR")
		}
		prefix, err := netip.ParsePrefix(configuredCIDR)
		if err != nil || !prefix.Addr().Is4() || prefix != prefix.Masked() {
			return fmt.Errorf("resourcePoolNetwork.reservedCidrs contains invalid canonical IPv4 CIDR %q", configuredCIDR)
		}
		if !prefixFullyContainedByPrivateRange(prefix, privateRanges) {
			return fmt.Errorf("resourcePoolNetwork.reservedCidrs contains non-private CIDR %q", configuredCIDR)
		}
	}
	return nil
}

func prefixFullyContainedByPrivateRange(prefix netip.Prefix, ranges []netip.Prefix) bool {
	start := prefix.Masked().Addr()
	startBytes := start.As4()
	startValue := uint64(startBytes[0])<<24 | uint64(startBytes[1])<<16 | uint64(startBytes[2])<<8 | uint64(startBytes[3])
	endValue := startValue + (uint64(1) << (32 - prefix.Bits())) - 1
	end := netip.AddrFrom4([4]byte{byte(endValue >> 24), byte(endValue >> 16), byte(endValue >> 8), byte(endValue)})
	for _, privateRange := range ranges {
		if privateRange.Contains(start) && privateRange.Contains(end) {
			return true
		}
	}
	return false
}

func GetMysqlDefaultConfig(cfg *Config) *mysql.Config {
	mysqlCfg := cfg.MysqlConfig

	defaultConfig := mysql.NewConfig()

	defaultConfig.Net = "tcp"
	defaultConfig.User = mysqlCfg.User
	defaultConfig.Addr = fmt.Sprintf("%s:%d", mysqlCfg.Host, mysqlCfg.Port)
	defaultConfig.Passwd = mysqlCfg.Password
	defaultConfig.DBName = mysqlCfg.Database
	defaultConfig.ParseTime = true
	defaultConfig.Timeout = 60 * time.Second
	defaultConfig.ReadTimeout = 30 * time.Second
	defaultConfig.WriteTimeout = 30 * time.Second
	defaultConfig.Params = map[string]string{"charset": "utf8mb4"}
	defaultConfig.Collation = "utf8mb4_bin"
	defaultConfig.TLSConfig = mysqlCfg.TLSConfigKey

	return defaultConfig
}

type loadConfigFunc func(string, string, string, int) (*Config, error)

func mergeMultipleConfigs(config *Config, loadConfig loadConfigFunc) error {
	err := viper.Unmarshal(config)
	if err != nil {
		return fmt.Errorf("failed unmarshal from viper: %w", err)
	}

	if config.ServerConfig.SecretStore != nil {
		s, err := loadConfig(
			config.ServerConfig.SecretStore.SecretKey,
			config.ServerConfig.SecretStore.Region,
			config.ServerConfig.SecretStore.Profile,
			config.ServerConfig.SecretStore.RetryCount,
		)
		if err != nil {
			return err
		}

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

	return nil
}

func loadRemoteConfig(smArn, region, profile string, retry int) (*Config, error) {
	secretConfig, err := getSecretFromAwsSecretManager(smArn, region, profile, retry)
	if err != nil {
		return nil, fmt.Errorf("failed to load secret config from aws: %w", err)
	}
	m := make(map[any]any)
	if err := yaml.Unmarshal([]byte(secretConfig), &m); err != nil {
		return nil, fmt.Errorf("failed to unmarshal secret config: %w", err)
	}

	var s Config

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

	return &s, nil
}

func getSecretFromAwsSecretManager(secretName, _, _ string, retryCount int) (string, error) {
	awsConfig, err := awsconfig.LoadDefaultConfig(context.TODO())
	if err != nil {
		panic("AWS configuration error, " + err.Error())
	}
	awsConfig.RetryMaxAttempts = retryCount
	secretsManagerCli := secretsmanager.NewFromConfig(awsConfig)

	input := &secretsmanager.GetSecretValueInput{
		SecretId:     pointer.String(secretName),
		VersionStage: pointer.String("AWSCURRENT"), // VersionStage defaults to AWSCURRENT if unspecified
	}
	result, err := secretsManagerCli.GetSecretValue(context.TODO(), input)
	if err != nil {
		return "", err
	}
	return *result.SecretString, nil
}
