// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// Package flags provides custom pflag.Value implementations for common use cases.
package flags

import (
	"fmt"
	"slices"
	"strings"

	"github.com/blang/semver/v4"
	"github.com/siderolabs/gen/maps"
	"github.com/siderolabs/gen/xslices"
	"github.com/spf13/pflag"
)

type choiceValue struct {
	value    string
	validate func(string) error
}

// Set implements pflag.Value interface.
func (v *choiceValue) Set(s string) error {
	err := v.validate(s)
	if err != nil {
		return err
	}

	v.value = s

	return nil
}

// Type implements pflag.Value interface.
func (v *choiceValue) Type() string { return "string" }

// String implements pflag.Value interface.
func (v *choiceValue) String() string { return v.value }

// StringChoice returns a [choiceValue] that validates the value against a set
// of choices. Only the last value will be used if multiple values are set.
func StringChoice(defaultValue string, otherChoices ...string) pflag.Value {
	return &choiceValue{
		value: defaultValue,
		validate: func(s string) error {
			choices := slices.Concat(otherChoices, []string{defaultValue})

			if slices.Contains(choices, s) {
				return nil
			}

			return fmt.Errorf("must be one of %v", choices)
		},
	}
}

type semverValue struct {
	value      semver.Version
	validators []SemverValidateFunc
}

// SemverValidateFunc allows setting restrictions on the version.
type SemverValidateFunc func(v semver.Version) error

// Set implements pflag.Value interface.
func (v *semverValue) Set(s string) error {
	vers, err := semver.ParseTolerant(s)
	if err != nil {
		return err
	}

	for _, validator := range v.validators {
		if err := validator(vers); err != nil {
			return err
		}
	}

	v.value = vers

	return nil
}

// Type implements pflag.Value interface.
func (v *semverValue) Type() string { return "semver" }

// String implements pflag.Value interface.
func (v *semverValue) String() string { return "v" + v.value.String() }

// Semver returns a pflag.Value that parses and stores a semantic version.
//
// Parsing is performed using semver.ParseTolerant. After parsing, any provided
// SemverValidateFunc validators are applied in order and may reject the version.
//
// The returned value is initialized with defaultValue, which is used until Set
// is called successfully.
func Semver(defaultValue string, validators ...SemverValidateFunc) pflag.Value {
	v, err := semver.ParseTolerant(defaultValue)
	if err != nil {
		panic(err)
	}

	return &semverValue{
		value:      v,
		validators: validators,
	}
}

type comparableStringer interface {
	~int32
	comparable
	fmt.Stringer
}

// PflagExtended extends pflag.Value with additional methods for retrieving the value as type T and getting valid string values.
type PflagExtended[T any] interface {
	pflag.Value

	Value() T
	Options() []string
}

type protoEnumValue[T comparableStringer] struct {
	value  T
	values map[string]int32
	names  map[int32]string
}

// Set implements pflag.Value interface.
func (v *protoEnumValue[T]) Set(s string) error {
	value, ok := v.values[strings.ToUpper(s)]
	if !ok {
		return fmt.Errorf("must be one of %v", v.Options())
	}

	v.value = T(value)

	return nil
}

// Type implements pflag.Value interface.
func (v *protoEnumValue[T]) Type() string { return "string" }

// String implements pflag.Value interface.
func (v *protoEnumValue[T]) String() string { return strings.ToLower(v.value.String()) }

// Value returns the enum value as the type T.
func (v *protoEnumValue[T]) Value() T {
	return v.value
}

// Options returns the valid string values for the enum.
func (v *protoEnumValue[T]) Options() []string {
	opts := xslices.Map(maps.Keys(v.values), strings.ToLower)

	slices.Sort(opts)

	return opts
}

// ProtoEnum returns a [protoEnumValue] that validates the value is correct.
// Only the last value will be used if multiple values are set.
func ProtoEnum[T comparableStringer](defaultValue T, values map[string]int32, names map[int32]string) PflagExtended[T] {
	return &protoEnumValue[T]{
		value:  defaultValue,
		values: values,
		names:  names,
	}
}
