// Package fflag provides a simple feature flag system.
//
// Feature names are lowercase and can only contain letters, numbers, undercores
// and dots.
//
// good: "foo", "foo_bar", "foo.bar"
// bad: "Foo", "foo-bar"
//
// A feature flag can be enabled by the user with an environment variable
// or by adding it to {ConfigDir}/feature.yaml
//
// I.e. CROWDSEC_FEATURE_FOO_BAR=true
// or in feature.yaml:
// ---
// - foo_bar
//
// If the variable is set to false, the feature can still be enabled
// in feature.yaml. Features cannot be disabled in the file.
//
// A feature flag can be deprecated or retired. A deprecated feature flag is
// still accepted but a warning is logged (only if a deprecation message is provided).
// A retired feature flag is ignored and an error is logged.
//
// The message is inteded to inform the user of the behavior
// that has been decided when the flag is/was finally retired.

package fflag

import (
	"errors"
	"fmt"
	"io"
	"os"
	"regexp"
	"sort"
	"strings"

	"github.com/goccy/go-yaml"
	"github.com/sirupsen/logrus"
)

var (
	ErrFeatureNameEmpty   = errors.New("name is empty")
	ErrFeatureNameCase    = errors.New("name is not lowercase")
	ErrFeatureNameInvalid = errors.New("invalid name (allowed a-z, 0-9, _, .)")
	ErrFeatureUnknown     = errors.New("unknown feature")
	ErrFeatureDeprecated  = errors.New("the flag is deprecated")
	ErrFeatureRetired     = errors.New("the flag is retired")
)

const (
	ActiveState     = iota // the feature can be enabled, and its description is logged (Info)
	DeprecatedState        // the feature can be enabled, and a deprecation message is logged (Warning)
	RetiredState           // the feature is ignored and a deprecation message is logged (Error)
)

type Feature struct {
	Name  string
	State int // active, deprecated, retired

	// Description should be a short sentence, explaining the feature.
	Description string

	// DeprecationMessage is used to inform the user of the behavior that has
	// been decided when the flag is/was finally retired.
	DeprecationMsg string

	enabled bool
}

func (f *Feature) IsEnabled() bool {
	return f.enabled
}

// Set enables or disables a feature flag
// It should not be called directly by the user, but by SetFromEnv or SetFromYaml
func (f *Feature) Set(value bool) error {
	// retired feature flags are ignored
	if f.State == RetiredState {
		return ErrFeatureRetired
	}

	f.enabled = value

	// deprecated feature flags are still accepted, but a warning is triggered.
	// We return an error but set the feature anyway.
	if f.State == DeprecatedState {
		return ErrFeatureDeprecated
	}

	return nil
}

// A register allows to enable features from the environment or a file
type FeatureRegister struct {
	EnvPrefix string
	features  map[string]*Feature
}

var featureNameRexp = regexp.MustCompile(`^[a-z0-9_.]+$`)

func validateFeatureName(featureName string) error {
	if featureName == "" {
		return ErrFeatureNameEmpty
	}

	if featureName != strings.ToLower(featureName) {
		return ErrFeatureNameCase
	}

	if !featureNameRexp.MatchString(featureName) {
		return ErrFeatureNameInvalid
	}

	return nil
}

func (fr *FeatureRegister) RegisterFeature(feat *Feature) error {
	if err := validateFeatureName(feat.Name); err != nil {
		return fmt.Errorf("feature flag '%s': %w", feat.Name, err)
	}

	if fr.features == nil {
		fr.features = make(map[string]*Feature)
	}

	fr.features[feat.Name] = feat

	return nil
}

func (fr *FeatureRegister) GetFeature(featureName string) (*Feature, error) {
	feat, ok := fr.features[featureName]
	if !ok {
		return feat, ErrFeatureUnknown
	}

	return feat, nil
}

func (fr *FeatureRegister) SetFromEnv(logger *logrus.Logger) error {
	for _, e := range os.Environ() {
		// ignore non-feature variables
		if !strings.HasPrefix(e, fr.EnvPrefix) {
			continue
		}

		// extract feature name and value
		pair := strings.SplitN(e, "=", 2)
		varName := pair[0]
		featureName := strings.ToLower(varName[len(fr.EnvPrefix):])
		value := pair[1]

		var enable bool

		switch value {
		case "true":
			enable = true
		case "false":
			enable = false
		default:
			logger.Errorf("Ignored envvar %s=%s: invalid value (must be 'true' or 'false')", varName, value)
			continue
		}

		feat, err := fr.GetFeature(featureName)
		if err != nil {
			logger.Errorf("Ignored envvar '%s': %s.", varName, err)
			continue
		}

		err = feat.Set(enable)

		switch {
		case errors.Is(err, ErrFeatureRetired):
			logger.Errorf("Ignored envvar '%s': %s. %s", varName, err, feat.DeprecationMsg)
			continue
		case errors.Is(err, ErrFeatureDeprecated):
			if feat.DeprecationMsg != "" {
				logger.Warningf("Envvar '%s': %s. %s", varName, err, feat.DeprecationMsg)
			}
		case err != nil:
			return err
		}

		logger.Debugf("Feature flag: %s=%t (from envvar). %s", featureName, enable, feat.Description)
	}

	return nil
}

func (fr *FeatureRegister) SetFromYaml(r io.Reader, logger *logrus.Logger) error {
	var cfg []string

	bys, err := io.ReadAll(r)
	if err != nil {
		return err
	}

	// parse config file
	if err := yaml.Unmarshal(bys, &cfg); err != nil {
		if !errors.Is(err, io.EOF) {
			return fmt.Errorf("failed to parse feature flags: %w", err)
		}

		logger.Debug("No feature flags in config file")
	}

	// set features
	for _, k := range cfg {
		feat, err := fr.GetFeature(k)
		if err != nil {
			logger.Errorf("Ignored feature flag '%s': %s", k, err)
			continue
		}

		err = feat.Set(true)

		switch {
		case errors.Is(err, ErrFeatureRetired):
			logger.Errorf("Ignored feature flag '%s': %s. %s", k, err, feat.DeprecationMsg)
			continue
		case errors.Is(err, ErrFeatureDeprecated):
			logger.Warningf("Feature '%s': %s. %s", k, err, feat.DeprecationMsg)
		case err != nil:
			return err
		}

		logger.Debugf("Feature flag: %s=true (from config file). %s", k, feat.Description)
	}

	return nil
}

func (fr *FeatureRegister) SetFromYamlFile(path string, logger *logrus.Logger) error {
	f, err := os.Open(path)
	if err != nil {
		if os.IsNotExist(err) {
			logger.Tracef("Feature flags config file '%s' does not exist", path)

			return nil
		}

		return fmt.Errorf("failed to open feature flags file: %w", err)
	}
	defer f.Close()

	logger.Debugf("Reading feature flags from %s", path)

	return fr.SetFromYaml(f, logger)
}

// GetEnabledFeatures returns the list of features that have been enabled by the user
func (fr *FeatureRegister) GetEnabledFeatures() []string {
	ret := make([]string, 0)

	for k, feat := range fr.features {
		if feat.IsEnabled() {
			ret = append(ret, k)
		}
	}

	sort.Strings(ret)

	return ret
}

// GetAllFeatures returns a slice of all the known features, ordered by name
func (fr *FeatureRegister) GetAllFeatures() []Feature {
	ret := make([]Feature, len(fr.features))

	i := 0
	for _, feat := range fr.features {
		ret[i] = *feat
		i++
	}

	sort.Slice(ret, func(i, j int) bool {
		return ret[i].Name < ret[j].Name
	})

	return ret
}
