package modules

import (
	"context"

	"github.com/tevino/abool"

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

var (
	moduleMgmtEnabled     = abool.NewBool(false)
	modulesChangeNotifyFn func(*Module)
)

// Enable enables the module. Only has an effect if module management
// is enabled.
func (m *Module) Enable() (changed bool) {
	return m.enabled.SetToIf(false, true)
}

// Disable disables the module. Only has an effect if module management
// is enabled.
func (m *Module) Disable() (changed bool) {
	return m.enabled.SetToIf(true, false)
}

// SetEnabled sets the module to the desired enabled state. Only has
// an effect if module management is enabled.
func (m *Module) SetEnabled(enable bool) (changed bool) {
	if enable {
		return m.Enable()
	}
	return m.Disable()
}

// Enabled returns whether or not the module is currently enabled.
func (m *Module) Enabled() bool {
	return m.enabled.IsSet()
}

// EnabledAsDependency returns whether or not the module is currently
// enabled as a dependency.
func (m *Module) EnabledAsDependency() bool {
	return m.enabledAsDependency.IsSet()
}

// EnableModuleManagement enables the module management functionality
// within modules. The supplied notify function will be called whenever
// the status of a module changes. The affected module will be in the
// parameter. You will need to manually enable modules, else nothing
// will start.
// EnableModuleManagement returns true if changeNotifyFn has been set
// and it has been called for the first time.
//
// Example:
//
//	EnableModuleManagement(func(m *modules.Module) {
//		// some module has changed ...
//		// do what ever you like
//
//		// Run the built-in module management
//		modules.ManageModules()
//	})
func EnableModuleManagement(changeNotifyFn func(*Module)) bool {
	if moduleMgmtEnabled.SetToIf(false, true) {
		modulesChangeNotifyFn = changeNotifyFn
		return true
	}
	return false
}

// DisableModuleManagement disables module management and returns the module
// system to the default start/stop behavior.
func DisableModuleManagement() {
	moduleMgmtEnabled.UnSet()
}

func (m *Module) notifyOfChange() {
	if moduleMgmtEnabled.IsSet() && modulesChangeNotifyFn != nil {
		m.StartWorker("notify of change", func(ctx context.Context) error {
			modulesChangeNotifyFn(m)
			return nil
		})
	}
}

// ManageModules triggers the module manager to react to recent changes of
// enabled modules.
func ManageModules() error {
	// check if enabled
	if !moduleMgmtEnabled.IsSet() {
		return nil
	}

	// lock mgmt
	mgmtLock.Lock()
	defer mgmtLock.Unlock()

	log.Info("modules: managing changes")

	// build new dependency tree
	buildEnabledTree()

	// stop unneeded modules
	lastErr := stopModules()
	if lastErr != nil {
		log.Warning(lastErr.Error())
	}

	// start needed modules
	err := startModules()
	if err != nil {
		log.Warning(err.Error())
		lastErr = err
	}

	log.Info("modules: finished managing")
	return lastErr
}

func buildEnabledTree() {
	// reset marked dependencies
	for _, m := range modules {
		m.enabledAsDependency.UnSet()
	}

	// mark dependencies
	for _, m := range modules {
		if m.enabled.IsSet() {
			m.markDependencies()
		}
	}
}

func (m *Module) markDependencies() {
	for _, dep := range m.depModules {
		if dep.enabledAsDependency.SetToIf(false, true) {
			dep.markDependencies()
		}
	}
}
