package config

import (
	"errors"
	"fmt"
	"math"
	"reflect"

	"github.com/safing/portbase/log"
)

type valueCache struct {
	stringVal      string
	stringArrayVal []string
	intVal         int64
	boolVal        bool
}

func (vc *valueCache) getData(opt *Option) interface{} {
	switch opt.OptType {
	case OptTypeBool:
		return vc.boolVal
	case OptTypeInt:
		return vc.intVal
	case OptTypeString:
		return vc.stringVal
	case OptTypeStringArray:
		return vc.stringArrayVal
	case optTypeAny:
		return nil
	default:
		return nil
	}
}

// isAllowedPossibleValue checks if value is defined as a PossibleValue
// in opt. If there are not possible values defined value is considered
// allowed and nil is returned. isAllowedPossibleValue ensure the actual
// value is an allowed primitiv value by using reflection to convert
// value and each PossibleValue to a comparable primitiv if possible.
// In case of complex value types isAllowedPossibleValue uses
// reflect.DeepEqual as a fallback.
func isAllowedPossibleValue(opt *Option, value interface{}) error {
	if opt.PossibleValues == nil {
		return nil
	}

	for _, val := range opt.PossibleValues {
		compareAgainst := val.Value
		valueType := reflect.TypeOf(value)

		// loading int's from the configuration JSON does not preserve the correct type
		// as we get float64 instead. Make sure to convert them before.
		if reflect.TypeOf(val.Value).ConvertibleTo(valueType) {
			compareAgainst = reflect.ValueOf(val.Value).Convert(valueType).Interface()
		}
		if compareAgainst == value {
			return nil
		}

		if reflect.DeepEqual(val.Value, value) {
			return nil
		}
	}

	return errors.New("value is not allowed")
}

// migrateValue runs all value migrations.
func migrateValue(option *Option, value any) any {
	for _, migration := range option.Migrations {
		newValue := migration(option, value)
		if newValue != value {
			log.Debugf("config: migrated %s value from %v to %v", option.Key, value, newValue)
		}
		value = newValue
	}
	return value
}

// validateValue ensures that value matches the expected type of option.
// It does not create a copy of the value!
func validateValue(option *Option, value interface{}) (*valueCache, *ValidationError) { //nolint:gocyclo
	if option.OptType != OptTypeStringArray {
		if err := isAllowedPossibleValue(option, value); err != nil {
			return nil, &ValidationError{
				Option: option.copyOrNil(),
				Err:    err,
			}
		}
	}

	var validated *valueCache
	switch v := value.(type) {
	case string:
		if option.OptType != OptTypeString {
			return nil, invalid(option, "expected type %s, got type %T", getTypeName(option.OptType), v)
		}
		if option.compiledRegex != nil {
			if !option.compiledRegex.MatchString(v) {
				return nil, invalid(option, "did not match validation regex")
			}
		}
		validated = &valueCache{stringVal: v}
	case []interface{}:
		vConverted := make([]string, len(v))
		for pos, entry := range v {
			s, ok := entry.(string)
			if !ok {
				return nil, invalid(option, "entry #%d is not a string", pos+1)
			}
			vConverted[pos] = s
		}
		// Call validation function again with converted value.
		var vErr *ValidationError
		validated, vErr = validateValue(option, vConverted)
		if vErr != nil {
			return nil, vErr
		}
	case []string:
		if option.OptType != OptTypeStringArray {
			return nil, invalid(option, "expected type %s, got type %T", getTypeName(option.OptType), v)
		}
		if option.compiledRegex != nil {
			for pos, entry := range v {
				if !option.compiledRegex.MatchString(entry) {
					return nil, invalid(option, "entry #%d did not match validation regex", pos+1)
				}

				if err := isAllowedPossibleValue(option, entry); err != nil {
					return nil, invalid(option, "entry #%d is not allowed", pos+1)
				}
			}
		}
		validated = &valueCache{stringArrayVal: v}
	case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, float32, float64:
		// uint64 is omitted, as it does not fit in a int64
		if option.OptType != OptTypeInt {
			return nil, invalid(option, "expected type %s, got type %T", getTypeName(option.OptType), v)
		}
		if option.compiledRegex != nil {
			// we need to use %v here so we handle float and int correctly.
			if !option.compiledRegex.MatchString(fmt.Sprintf("%v", v)) {
				return nil, invalid(option, "did not match validation regex")
			}
		}
		switch v := value.(type) {
		case int:
			validated = &valueCache{intVal: int64(v)}
		case int8:
			validated = &valueCache{intVal: int64(v)}
		case int16:
			validated = &valueCache{intVal: int64(v)}
		case int32:
			validated = &valueCache{intVal: int64(v)}
		case int64:
			validated = &valueCache{intVal: v}
		case uint:
			validated = &valueCache{intVal: int64(v)}
		case uint8:
			validated = &valueCache{intVal: int64(v)}
		case uint16:
			validated = &valueCache{intVal: int64(v)}
		case uint32:
			validated = &valueCache{intVal: int64(v)}
		case float32:
			// convert if float has no decimals
			if math.Remainder(float64(v), 1) == 0 {
				validated = &valueCache{intVal: int64(v)}
			} else {
				return nil, invalid(option, "failed to convert float32 to int64")
			}
		case float64:
			// convert if float has no decimals
			if math.Remainder(v, 1) == 0 {
				validated = &valueCache{intVal: int64(v)}
			} else {
				return nil, invalid(option, "failed to convert float64 to int64")
			}
		default:
			return nil, invalid(option, "internal error")
		}
	case bool:
		if option.OptType != OptTypeBool {
			return nil, invalid(option, "expected type %s, got type %T", getTypeName(option.OptType), v)
		}
		validated = &valueCache{boolVal: v}
	default:
		return nil, invalid(option, "invalid option value type: %T", value)
	}

	// Check if there is an additional function to validate the value.
	if option.ValidationFunc != nil {
		var err error
		switch option.OptType {
		case optTypeAny:
			err = errors.New("internal error")
		case OptTypeString:
			err = option.ValidationFunc(validated.stringVal)
		case OptTypeStringArray:
			err = option.ValidationFunc(validated.stringArrayVal)
		case OptTypeInt:
			err = option.ValidationFunc(validated.intVal)
		case OptTypeBool:
			err = option.ValidationFunc(validated.boolVal)
		}
		if err != nil {
			return nil, &ValidationError{
				Option: option.copyOrNil(),
				Err:    err,
			}
		}
	}

	return validated, nil
}

// ValidationError error holds details about a config option value validation error.
type ValidationError struct {
	Option *Option
	Err    error
}

// Error returns the formatted error.
func (ve *ValidationError) Error() string {
	return fmt.Sprintf("validation of %s failed: %s", ve.Option.Key, ve.Err)
}

// Unwrap returns the wrapped error.
func (ve *ValidationError) Unwrap() error {
	return ve.Err
}

func invalid(option *Option, format string, a ...interface{}) *ValidationError {
	return &ValidationError{
		Option: option.copyOrNil(),
		Err:    fmt.Errorf(format, a...),
	}
}
