package featureflag

import (
	"context"
	"fmt"
	"regexp"

	"go.opentelemetry.io/otel/attribute"
	otelmetric "go.opentelemetry.io/otel/metric"
)

// Feature flags must contain at least 2 characters. Can only contain lowercase letters,
// digits, and '_'. They must start with a letter, and cannot end with '_'.
// Feature flag name would be used to construct the corresponding metadata key, so:
//   - Only characters allowed by grpc metadata keys can be used and uppercase letters
//     would be normalized to lowercase, see
//     https://pkg.go.dev/google.golang.org/grpc/metadata#New
//   - It is critical that feature flags don't contain a dash, because the client converts
//     dashes to underscores when converting a feature flag's name to the metadata key,
//     and vice versa. The name wouldn't round-trip in case it had underscores and must
//     thus use dashes instead.
var ffNameRegexp = regexp.MustCompile(`^[a-z][a-z0-9_]*[a-z0-9]$`)

const (
	checksCounterName = "kas_feature_flag_checks"
)

type FeatureFlag struct {
	Name    string
	Default bool

	// pre-computed metric options so we don't have to do it on runtime and allocate there.
	fromDefaultMetricOptions []otelmetric.AddOption
	enabledMetricOptions     []otelmetric.AddOption
	disabledMetricOptions    []otelmetric.AddOption
}

type Set struct {
	ffs          map[string]bool
	checkCounter otelmetric.Int64Counter
}

func NewCheckCounter(meter otelmetric.Meter) (otelmetric.Int64Counter, error) {
	counter, err := meter.Int64Counter(
		checksCounterName,
		otelmetric.WithDescription("The number of feature flag checks"),
	)
	if err != nil {
		return nil, err
	}
	return counter, nil
}

func NewFeatureFlag(name string, defaultValue bool) *FeatureFlag {
	if !ffNameRegexp.MatchString(name) {
		panic(fmt.Errorf("invalid feature flag name %q, check the featureflag package for guidance on the feature flag names", name))
	}

	nameAttr := attribute.String("name", name)
	return &FeatureFlag{
		Name:                     name,
		Default:                  defaultValue,
		fromDefaultMetricOptions: []otelmetric.AddOption{otelmetric.WithAttributeSet(attribute.NewSet(nameAttr, attribute.Bool("enabled", defaultValue), attribute.Bool("from_default", true)))},
		enabledMetricOptions:     []otelmetric.AddOption{otelmetric.WithAttributeSet(attribute.NewSet(nameAttr, attribute.Bool("enabled", true)))},
		disabledMetricOptions:    []otelmetric.AddOption{otelmetric.WithAttributeSet(attribute.NewSet(nameAttr, attribute.Bool("enabled", false)))},
	}
}

func NewSet(ffs map[string]bool, checkCounter otelmetric.Int64Counter) Set {
	return Set{ffs: ffs, checkCounter: checkCounter}
}

func (s Set) IsEnabled(ff *FeatureFlag) bool {
	enabled, ok := s.ffs[ff.Name]
	if !ok {
		// we don't know about this feature flag,
		// thus we return the configured default
		s.checkCounter.Add(context.Background(), 1, ff.fromDefaultMetricOptions...)
		return ff.Default
	}

	if enabled {
		s.checkCounter.Add(context.Background(), 1, ff.enabledMetricOptions...)
	} else {
		s.checkCounter.Add(context.Background(), 1, ff.disabledMetricOptions...)
	}

	return enabled
}

func (s Set) IsDisabled(ff *FeatureFlag) bool {
	return !s.IsEnabled(ff)
}

func (s Set) ToMap() map[string]bool {
	return s.ffs
}
