package config

import (
	"context"
	"sync"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/client"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/secretsmanager"
	"github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
	"github.com/creasty/defaults"
	"github.com/fsnotify/fsnotify"
	"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"
	"go.uber.org/zap"
	"gopkg.in/yaml.v2"
)

// SecretFetcher is an abstract interface for fetching secrets.
type SecretFetcher interface {
	GetSecret(ctx context.Context, secretKey, region string) (string, error)
}

// Provider is the interface for accessing the central configuration.
type Provider interface {
	// Get returns the current configuration.
	Get() *CentralConfig
}

// awsSecretFetcher is the AWS Secrets Manager implementation of SecretFetcher.
type awsSecretFetcher struct {
	// Cache sessions to avoid creating a new one for every request.
	sessCache map[string]secretsmanageriface.SecretsManagerAPI
	sessLock  sync.Mutex
}

// newAWSSecretFetcher creates an instance of the AWS secret fetcher.
func newAWSSecretFetcher() SecretFetcher {
	return &awsSecretFetcher{
		sessCache: make(map[string]secretsmanageriface.SecretsManagerAPI),
	}
}

// GetSecret implements the SecretFetcher interface.
func (a *awsSecretFetcher) GetSecret(ctx context.Context, secretName, region string) (string, error) {
	svc, err := a.getClient(region)
	if err != nil {
		return "", errors.Annotatef(err, "failed to create aws session for region %s", region)
	}

	input := &secretsmanager.GetSecretValueInput{
		SecretId:     aws.String(secretName),
		VersionStage: aws.String("AWSCURRENT"),
	}

	result, err := svc.GetSecretValueWithContext(ctx, input)
	if err != nil {
		return "", errors.Annotatef(err, "failed to get secret value for %s", secretName)
	}

	if result.SecretString == nil {
		return "", errors.Errorf("secret string is nil for %s", secretName)
	}
	return *result.SecretString, nil
}

// getClient retrieves or creates a new AWS client from the cache.
func (a *awsSecretFetcher) getClient(region string) (secretsmanageriface.SecretsManagerAPI, error) {
	a.sessLock.Lock()
	defer a.sessLock.Unlock()

	if svc, ok := a.sessCache[region]; ok {
		return svc, nil
	}

	awsConfig := aws.NewConfig().WithRegion(region)
	awsConfig.Retryer = client.DefaultRetryer{NumMaxRetries: 3}
	opts := session.Options{Config: *awsConfig}
	sess, err := session.NewSessionWithOptions(opts)
	if err != nil {
		return nil, err
	}

	svc := secretsmanager.New(sess)
	a.sessCache[region] = svc
	return svc, nil
}

// Provider is our config manager. It holds and reloads the config safely.
type providerImpl struct {
	v        *viper.Viper
	config   *CentralConfig
	lock     sync.RWMutex
	validate *validator.Validate
	fetcher  SecretFetcher
}

// newProvider creates and loads the configuration. This is the actual "init" logic.
// It is designed to be testable by injecting the SecretFetcher.
func newProvider(configPath string, fetcher SecretFetcher) (*providerImpl, error) {
	v := viper.New()
	v.SetConfigFile(configPath)

	p := &providerImpl{
		v:        v,
		validate: validator.New(),
		fetcher:  fetcher,
	}

	// Perform the initial load
	if err := p.load(); err != nil {
		return nil, errors.Annotate(err, "initial config load failed")
	}

	// Start watching for changes
	p.watch()

	return p, nil
}

// load is the core pipeline for loading configuration.
// Flow: Defaults -> Config File -> Remote Secrets -> Validation
func (p *providerImpl) load() error {
	// 1. Create a new config instance and apply defaults
	conf := &CentralConfig{}
	if err := defaults.Set(conf); err != nil {
		return errors.Annotate(err, "failed to set defaults")
	}

	// 2. Read the config file
	if err := p.v.ReadInConfig(); err != nil {
		// Allow "file not found" error, so we can rely purely on defaults/secrets
		if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
			return errors.Annotate(err, "failed to read config file")
		}
		// If file not found, log a warning but continue
		log.L().Warn("config file not found, proceeding with defaults/secrets", zap.String("file", p.v.ConfigFileUsed()))
	} else {
		// 3. If file exists, unmarshal into the config (overwriting defaults)
		if err := p.v.Unmarshal(conf); err != nil {
			return errors.Annotate(err, "failed to unmarshal from viper")
		}
	}

	// 4. Load and merge remote secrets (overwriting file config)
	if conf.ServerConfig.SecretStore != nil {
		secretConf, err := p.loadSecrets(context.Background(), conf.ServerConfig.SecretStore)
		if err != nil {
			return errors.Annotate(err, "failed to load secret config")
		}
		// Use mergo.Merge to deep-merge, WithOverride ensures values from secrets win
		if err := mergo.Merge(conf, secretConf, mergo.WithOverride); err != nil {
			return errors.Annotate(err, "failed to merge secret config")
		}
	}

	// 5. Validate the final configuration
	if err := p.validate.Struct(conf); err != nil {
		return errors.Annotate(err, "config validation failed")
	}

	// 6. Set the new config in a thread-safe way
	p.lock.Lock()
	p.config = conf
	p.lock.Unlock()

	log.L().Debug("successfully loaded and validated new config", zap.Reflect("config", conf))
	return nil
}

// loadSecrets is a helper to handle the logic of fetching and parsing secrets.
func (p *providerImpl) loadSecrets(ctx context.Context, store *SecretConfig) (*CentralConfig, error) {
	if store.SecretKey == "" || store.Region == "" {
		return nil, errors.New("secretkey and region must be set in config to load secrets")
	}

	secretYaml, err := p.fetcher.GetSecret(ctx, store.SecretKey, store.Region)
	if err != nil {
		return nil, errors.Annotate(err, "failed to get secret from fetcher")
	}

	m := make(map[interface{}]interface{})
	if err := yaml.Unmarshal([]byte(secretYaml), &m); err != nil {
		return nil, errors.Annotate(err, "failed to unmarshal secret yaml")
	}

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

	return &secretConfig, nil
}

// watch sets up viper to monitor the config file for changes.
func (p *providerImpl) watch() {
	p.v.OnConfigChange(func(e fsnotify.Event) {
		log.L().Info("config file changed, reloading...", zap.String("file", e.Name))
		// Trigger the p.load() pipeline
		if err := p.load(); err != nil {
			// If reload fails, we keep the old config and just log the error
			log.L().Error("failed to reload config, old config is still in use", zap.Error(err))
		} else {
			log.L().Info("config reloaded successfully")
		}
	})
	p.v.WatchConfig()
}

// Get returns a *read-only* copy of the current config.
// It uses a RWMutex to ensure thread safety.
func (p *providerImpl) Get() *CentralConfig {
	p.lock.RLock()
	defer p.lock.RUnlock()
	// The returned config should NOT be modified as it is shared across goroutines.
	return p.config
}

var (
	globalProvider Provider
	initOnce       sync.Once
)

// InitConfig initializes the global config provider with the real (AWS) secret fetcher.
func InitConfig(path string) error {
	var err error
	initOnce.Do(func() {
		fetcher := newAWSSecretFetcher()
		globalProvider, err = newProvider(path, fetcher)
	})
	if err != nil {
		return errors.Annotatef(err, "failed to init global config provider")
	}
	return nil
}

// Global returns the global, thread-safe configuration.
func Global() *CentralConfig {
	if globalProvider == nil {
		log.L().Panic("config.InitConfig() was not called before config.Global()")
	}
	return globalProvider.Get()
}
