package dynamicconfig

import (
	"context"
	"fmt"
	"slices"
	"strings"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

// DynamicConfig exposes read operations for configuration that can change
// at runtime without restarting the process. The implementation backs this
// with a remote ConfigMap and periodically refreshes it.
//
// Keys of maps in the configuration are case-insensitive. All keys are
// normalized to lower-case during unmarshalling and lookups.
type DynamicConfig interface {
	// GetByDimensions returns the value set associated with configName for the
	// provided set of dimensions. If multiple items match, the last (highest
	// priority) match wins. If nothing matches or the configName is unknown,
	// it returns nil.
	GetByDimensions(ctx context.Context, configName string, dimensions map[string]string) map[string]string
}

// config is the concrete implementation of DynamicConfig.
//
// The DimensionConfigItems map holds a list of dimension-based items for each
// configName. The order of items matters: the first match has the lowest
// priority and the last match overrides previous matches.
//
// Note: keys in this map are case-insensitive (they are stored in lower-case).
type config struct {
	// configName -> items
	// for []Item, the first match is the lowest priority, the last match is the highest priority.
	// suggest put default config(if you have) in the first item, override at later items
	// Notice: key of map is case-insensitive
	DimensionConfigItems map[string][]DimensionConfigItem `mapstructure:"dimensionConfig"`
}

// DimensionConfigItem represents one rule/value pair for a particular configName.
// If the item matches the provided dimensions, its Values become the candidate
// result. Later matching items overwrite earlier ones.
type DimensionConfigItem struct {
	// Rules describes the matching criteria for dimensions. The key is the
	// dimension name, case-insensitive.
	// Notice: key of map is case-insensitive
	Rules map[string]DimensionConfigRule `mapstructure:"rules"`
	// Values contains arbitrary user-defined key/value pairs returned when this
	// item matches. Keys and values are case-insensitive; keys are normalized to
	// lower-case during unmarshalling.
	// Notice: key and value of map is case-insensitive
	Values map[string]string `mapstructure:"values"`
}

// DimensionConfigRule governs how a single dimension is matched.
// If AllEnabled is true, any value for that dimension matches. Otherwise,
// the value must be explicitly listed in EnabledFor.
type DimensionConfigRule struct {
	// if allEnabled is true, it will be enabled for all the values of this dimension
	AllEnabled bool `mapstructure:"allEnabled"`
	// list all the enabled values of this dimension with AllEnabled is false
	EnabledFor []string `mapstructure:"enabledFor"`
}

// GetByDimensions returns the values of the configName with the last hit rule.
// It logs the last matched item and returns its Values. If the config name is
// unknown, or no items match, it returns nil.
//
// Notice: keys of maps handled by viper are case-insensitive; they are
// unmarshalled and compared in lower-case.
func (c *config) GetByDimensions(
	ctx context.Context,
	configName string,
	dimensions map[string]string,
) map[string]string {
	configName = strings.ToLower(configName)
	items, ok := c.DimensionConfigItems[configName]
	if !ok {
		return nil
	}

	loweredDimensions := make(map[string]string)
	for k, v := range dimensions {
		loweredDimensions[strings.ToLower(k)] = v
	}

	var values map[string]string
	for _, item := range items {
		matched, err := areDimensionsMatched(item, loweredDimensions)
		if err != nil {
			tilog.Error(ctx, "error checking dimension config matched", zap.Error(err),
				zap.String("configName", configName), zap.Any("dimensions", dimensions), zap.Any("item", item))
			continue
		}
		if matched {
			tilog.Info(
				ctx,
				"matched dimension config",
				zap.String("configName", configName),
				zap.Any("dimensions", dimensions),
				zap.Any("item", item),
			)
			values = item.Values
		}
	}
	return values
}

// areDimensionsMatched validates and evaluates whether the provided dimensions
// satisfy the item's rules.
//
// Returns true when:
//   - The set of dimension keys equals the set of rule keys; and
//   - For each rule, either AllEnabled is true or the dimension's value is
//     present in EnabledFor.
//
// Returns (false, error) when dimensions or rules are missing/mismatched. Returns
// (false, nil) when validation passes but one or more rule checks fail.
func areDimensionsMatched(item DimensionConfigItem, dimensions map[string]string) (bool, error) {
	if len(item.Rules) == 0 || len(dimensions) == 0 {
		return false, fmt.Errorf("no dimensions found in item or input")
	}

	// validate the dimensions
	if len(dimensions) != len(item.Rules) {
		return false, fmt.Errorf(
			"dimensions length mismatch, not all dimensions are or there are not defined dimensions",
		)
	}
	for dimensionName := range dimensions {
		if _, ok := item.Rules[dimensionName]; !ok {
			return false, fmt.Errorf("dimension %s not found in item", dimensionName)
		}
	}

	// if exist one rule not matched, return false
	for dimensionName, rule := range item.Rules {
		if rule.AllEnabled {
			continue
		}
		if !slices.Contains(rule.EnabledFor, dimensions[dimensionName]) {
			return false, nil
		}
	}

	return true, nil
}
