package dynamicconfig

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/spf13/viper"
	ov "github.com/tidbcloud/config-service/oneconfig/viper"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

// Loader loads and serves dynamic configuration from a remote source.
//
// Implementations are safe for concurrent use. Call GetConfig whenever you
// need configuration, rather than caching the returned value, to ensure you
// always consume the most recent snapshot.
//
// See NewDynamicLoader for details about where the config is read from and
// the refresh behavior.
type Loader interface {
	// GetConfig returns the current snapshot of dynamic configuration.
	// The returned value reflects the last successfully loaded config and will
	// be updated periodically in the background. Do not cache it in long-lived
	// variables; call this method each time you need configuration.
	GetConfig() DynamicConfig
}

const (
	// defaultDynamicConfigPath is the default "<configmap>/<file>" path used by
	// the remote config provider when no explicit path is given to NewDynamicLoader.
	// It corresponds to a Kubernetes ConfigMap named "dynamic-config" with a key
	// "config.yaml".
	defaultDynamicConfigPath = "dynamic-config/config.yaml"
)

type dynamicLoader struct {
	config         DynamicConfig
	configPath     string
	reloadInterval time.Duration
	lock           *sync.Mutex
}

// NewDynamicLoader creates a Loader that reads dynamic configuration from a
// remote provider and refreshes it at a fixed interval.
//
// Parameters:
//   - ctx: used for logging. The loader starts a background goroutine that
//     periodically fetches and applies updates for the lifetime of the process.
//   - configPath: the remote path in the form "<configmap>/<file>", e.g.
//     "dynamic-config/config.yaml". If empty, defaultDynamicConfigPath is used.
//   - reloadInterval: how often to check and apply updates. If zero, a default
//     of 5 seconds is used.
//
// Under the hood this uses viper with the OneConfig remote provider to read a
// YAML document and unmarshal it into the internal config representation.
//
// The configPath is composed of configmap name and the file name in configmap
// an example configmap as following, the configPath of the following configmap is "dynamic-config/config.yaml"
/*
apiVersion: v1
kind: ConfigMap
metadata:
  name: dynamic-config
data:
  config.yaml: |-
    dimensionConfig:
      configName1:
        - rules:
            region:
              enabledFor: ["us-west-1"]
            ... other dimensions
          values:
            config_source: "first_rule"
            priority: "1"
        ... other rules
      configName2:
        - rules:
            region:
              enabledFor: ["us-west-1", "us-east-1"]
            environment:
              enabledFor: ["production", "staging"]
            ... other dimensions
          values:
            feature_enabled: "true"
            max_throughput: "1000"
            security_level: "high"
        ... other rules
      ... other configName
*/
func NewDynamicLoader(ctx context.Context, configPath string, reloadInterval time.Duration) (Loader, error) {
	if configPath == "" {
		configPath = defaultDynamicConfigPath
	}
	if reloadInterval == 0 {
		reloadInterval = 5 * time.Second
	}

	loader := &dynamicLoader{
		configPath:     configPath,
		reloadInterval: reloadInterval,
		lock:           &sync.Mutex{},
	}

	err := loader.start(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to start dimension config loader: %w", err)
	}

	return loader, nil
}

// GetConfig returns the current snapshot of configuration.
// The snapshot reflects the last successful load from the remote source and is
// updated periodically by a background goroutine. This method is safe for
// concurrent use.
func (l *dynamicLoader) GetConfig() DynamicConfig {
	l.lock.Lock()
	defer l.lock.Unlock()
	return l.config
}

func (l *dynamicLoader) start(ctx context.Context) error {
	viper.SetConfigType("yaml")
	err := viper.AddRemoteProvider(ov.Provider, ov.Endpoint, l.configPath)
	if err != nil {
		return fmt.Errorf("failed to set remote dynamic config: %w", err)
	}

	err = viper.ReadRemoteConfig()
	if err != nil {
		return fmt.Errorf("failed to read remote dynamic config: %w", err)
	}

	err = l.load(ctx)
	if err != nil {
		return fmt.Errorf("failed to load remote dynamic config: %w", err)
	}

	ticker := time.NewTicker(l.reloadInterval)
	// open a goroutine to watch remote changes forever
	go func() {
		defer ticker.Stop()
		for range ticker.C {
			// reload org allowlist
			err := viper.WatchRemoteConfig()
			if err != nil {
				tilog.Error(ctx, "unable to watch remote dynamic config", zap.Error(err))
				continue
			}

			err = l.load(ctx)
			if err != nil {
				tilog.Error(ctx, "unable to reload dynamic config", zap.Error(err))
				continue
			}
			tilog.Info(ctx, "reload dynamic config success")
		}
	}()

	return nil
}

func (l *dynamicLoader) load(ctx context.Context) error {
	l.lock.Lock()
	defer l.lock.Unlock()

	var cfg config

	err := viper.Unmarshal(&cfg)
	if err != nil {
		return fmt.Errorf("failed to unmarshal dynamic config: %w", err)
	}

	l.config = &cfg
	tilog.Info(ctx, "loaded dynamic config", zap.String("configPath", l.configPath))
	return nil
}
