package config

import (
	"errors"
	"sync"

	"github.com/tevino/abool"
)

var (
	// ErrInvalidJSON is returned by SetConfig and SetDefaultConfig if they receive invalid json.
	ErrInvalidJSON = errors.New("json string invalid")

	// ErrInvalidOptionType is returned by SetConfigOption and SetDefaultConfigOption if given an unsupported option type.
	ErrInvalidOptionType = errors.New("invalid option value type")

	validityFlag     = abool.NewBool(true)
	validityFlagLock sync.RWMutex
)

// getValidityFlag returns a flag that signifies if the configuration has been changed. This flag must not be changed, only read.
func getValidityFlag() *abool.AtomicBool {
	validityFlagLock.RLock()
	defer validityFlagLock.RUnlock()
	return validityFlag
}

// signalChanges marks the configs validtityFlag as dirty and eventually
// triggers a config change event.
func signalChanges() {
	// reset validity flag
	validityFlagLock.Lock()
	validityFlag.SetTo(false)
	validityFlag = abool.NewBool(true)
	validityFlagLock.Unlock()

	module.TriggerEvent(ChangeEvent, nil)
}

// ValidateConfig validates the given configuration and returns all validation
// errors as well as whether the given configuration contains unknown keys.
func ValidateConfig(newValues map[string]interface{}) (validationErrors []*ValidationError, requiresRestart bool, containsUnknown bool) {
	// RLock the options because we are not adding or removing
	// options from the registration but rather only checking the
	// options value which is guarded by the option's lock itself.
	optionsLock.RLock()
	defer optionsLock.RUnlock()

	var checked int
	for key, option := range options {
		newValue, ok := newValues[key]
		if ok {
			checked++

			func() {
				option.Lock()
				defer option.Unlock()

				newValue = migrateValue(option, newValue)
				_, err := validateValue(option, newValue)
				if err != nil {
					validationErrors = append(validationErrors, err)
				}

				if option.RequiresRestart {
					requiresRestart = true
				}
			}()
		}
	}

	return validationErrors, requiresRestart, checked < len(newValues)
}

// ReplaceConfig sets the (prioritized) user defined config.
func ReplaceConfig(newValues map[string]interface{}) (validationErrors []*ValidationError, requiresRestart bool) {
	// RLock the options because we are not adding or removing
	// options from the registration but rather only update the
	// options value which is guarded by the option's lock itself.
	optionsLock.RLock()
	defer optionsLock.RUnlock()

	for key, option := range options {
		newValue, ok := newValues[key]

		func() {
			option.Lock()
			defer option.Unlock()

			option.activeValue = nil
			if ok {
				newValue = migrateValue(option, newValue)
				valueCache, err := validateValue(option, newValue)
				if err == nil {
					option.activeValue = valueCache
				} else {
					validationErrors = append(validationErrors, err)
				}
			}
			handleOptionUpdate(option, true)

			if option.RequiresRestart {
				requiresRestart = true
			}
		}()
	}

	signalChanges()

	return validationErrors, requiresRestart
}

// ReplaceDefaultConfig sets the (fallback) default config.
func ReplaceDefaultConfig(newValues map[string]interface{}) (validationErrors []*ValidationError, requiresRestart bool) {
	// RLock the options because we are not adding or removing
	// options from the registration but rather only update the
	// options value which is guarded by the option's lock itself.
	optionsLock.RLock()
	defer optionsLock.RUnlock()

	for key, option := range options {
		newValue, ok := newValues[key]

		func() {
			option.Lock()
			defer option.Unlock()

			option.activeDefaultValue = nil
			if ok {
				newValue = migrateValue(option, newValue)
				valueCache, err := validateValue(option, newValue)
				if err == nil {
					option.activeDefaultValue = valueCache
				} else {
					validationErrors = append(validationErrors, err)
				}
			}
			handleOptionUpdate(option, true)

			if option.RequiresRestart {
				requiresRestart = true
			}
		}()
	}

	signalChanges()

	return validationErrors, requiresRestart
}

// SetConfigOption sets a single value in the (prioritized) user defined config.
func SetConfigOption(key string, value any) error {
	return setConfigOption(key, value, true)
}

func setConfigOption(key string, value any, push bool) (err error) {
	option, err := GetOption(key)
	if err != nil {
		return err
	}

	option.Lock()
	if value == nil {
		option.activeValue = nil
	} else {
		value = migrateValue(option, value)
		valueCache, vErr := validateValue(option, value)
		if vErr == nil {
			option.activeValue = valueCache
		} else {
			err = vErr
		}
	}

	// Add the "restart pending" annotation if the settings requires a restart.
	if option.RequiresRestart {
		option.setAnnotation(RestartPendingAnnotation, true)
	}

	handleOptionUpdate(option, push)
	option.Unlock()

	if err != nil {
		return err
	}

	// finalize change, activate triggers
	signalChanges()

	return SaveConfig()
}

// SetDefaultConfigOption sets a single value in the (fallback) default config.
func SetDefaultConfigOption(key string, value interface{}) error {
	return setDefaultConfigOption(key, value, true)
}

func setDefaultConfigOption(key string, value interface{}, push bool) (err error) {
	option, err := GetOption(key)
	if err != nil {
		return err
	}

	option.Lock()
	if value == nil {
		option.activeDefaultValue = nil
	} else {
		value = migrateValue(option, value)
		valueCache, vErr := validateValue(option, value)
		if vErr == nil {
			option.activeDefaultValue = valueCache
		} else {
			err = vErr
		}
	}

	// Add the "restart pending" annotation if the settings requires a restart.
	if option.RequiresRestart {
		option.setAnnotation(RestartPendingAnnotation, true)
	}

	handleOptionUpdate(option, push)
	option.Unlock()

	if err != nil {
		return err
	}

	// finalize change, activate triggers
	signalChanges()

	// Do not save the configuration, as it only saves the active values, not the
	// active default value.
	return nil
}
