package subsystems

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/tevino/abool"

	"github.com/safing/portbase/config"
	"github.com/safing/portbase/database/record"
	"github.com/safing/portbase/modules"
	"github.com/safing/portbase/runtime"
)

var (
	// ErrManagerStarted is returned when subsystem registration attempt
	// occurs after the manager has been started.
	ErrManagerStarted = errors.New("subsystem manager already started")
	// ErrDuplicateSubsystem is returned when the subsystem to be registered
	// is alreadey known (duplicated subsystem ID).
	ErrDuplicateSubsystem = errors.New("subsystem is already registered")
)

// Manager manages subsystems, provides access via a runtime
// value providers and can takeover module management.
type Manager struct {
	l              sync.RWMutex
	subsys         map[string]*Subsystem
	pushUpdate     runtime.PushFunc
	immutable      *abool.AtomicBool
	debounceUpdate *abool.AtomicBool
	runtime        *runtime.Registry
}

// NewManager returns a new subsystem manager that registers
// itself at rtReg.
func NewManager(rtReg *runtime.Registry) (*Manager, error) {
	mng := &Manager{
		subsys:         make(map[string]*Subsystem),
		immutable:      abool.New(),
		debounceUpdate: abool.New(),
	}

	push, err := rtReg.Register("subsystems/", runtime.SimpleValueGetterFunc(mng.Get))
	if err != nil {
		return nil, err
	}

	mng.pushUpdate = push
	mng.runtime = rtReg

	return mng, nil
}

// Start starts managing subsystems. Note that it's not possible
// to define new subsystems once Start() has been called.
func (mng *Manager) Start() error {
	mng.immutable.Set()

	seen := make(map[string]struct{}, len(mng.subsys))
	configKeyPrefixes := make(map[string]*Subsystem, len(mng.subsys))
	// mark all sub-systems as seen. This prevents sub-systems
	// from being added as a sub-systems dependency in addAndMarkDependencies.
	for _, sub := range mng.subsys {
		seen[sub.module.Name] = struct{}{}
		configKeyPrefixes[sub.ConfigKeySpace] = sub
	}

	// aggregate all modules dependencies (and the subsystem module itself)
	// into the Modules slice. Configuration options form dependent modules
	// will be marked using config.SubsystemAnnotation if not already set.
	for _, sub := range mng.subsys {
		sub.Modules = append(sub.Modules, statusFromModule(sub.module))
		sub.addDependencies(sub.module, seen)
	}

	// Annotate all configuration options with their respective subsystem.
	_ = config.ForEachOption(func(opt *config.Option) error {
		subsys, ok := configKeyPrefixes[opt.Key]
		if !ok {
			return nil
		}

		// Add a new subsystem annotation is it is not already set!
		opt.AddAnnotation(config.SubsystemAnnotation, subsys.ID)

		return nil
	})

	return nil
}

// Get implements runtime.ValueProvider.
func (mng *Manager) Get(keyOrPrefix string) ([]record.Record, error) {
	mng.l.RLock()
	defer mng.l.RUnlock()

	dbName := mng.runtime.DatabaseName()
	records := make([]record.Record, 0, len(mng.subsys))
	for _, subsys := range mng.subsys {
		subsys.Lock()
		if !subsys.KeyIsSet() {
			subsys.SetKey(dbName + ":subsystems/" + subsys.ID)
		}
		if strings.HasPrefix(subsys.DatabaseKey(), keyOrPrefix) {
			records = append(records, subsys)
		}
		subsys.Unlock()
	}

	// make sure the order is always the same
	sort.Sort(bySubsystemID(records))

	return records, nil
}

// Register registers a new subsystem. The given option must be a bool option.
// Should be called in init() directly after the modules.Register() function.
// The config option must not yet be registered and will be registered for
// you. Pass a nil option to force enable.
//
// TODO(ppacher): IMHO the subsystem package is not responsible of registering
// the "toggle option". This would also remove runtime
// dependency to the config package. Users should either pass
// the BoolOptionFunc and the expertise/release level directly
// or just pass the configuration key so those information can
// be looked up by the registry.
func (mng *Manager) Register(id, name, description string, module *modules.Module, configKeySpace string, option *config.Option) error {
	mng.l.Lock()
	defer mng.l.Unlock()

	if mng.immutable.IsSet() {
		return ErrManagerStarted
	}

	if _, ok := mng.subsys[id]; ok {
		return ErrDuplicateSubsystem
	}

	s := &Subsystem{
		ID:             id,
		Name:           name,
		Description:    description,
		ConfigKeySpace: configKeySpace,
		module:         module,
		toggleOption:   option,
	}

	s.CreateMeta()

	if s.toggleOption != nil {
		s.ToggleOptionKey = s.toggleOption.Key
		s.ExpertiseLevel = s.toggleOption.ExpertiseLevel
		s.ReleaseLevel = s.toggleOption.ReleaseLevel

		if err := config.Register(s.toggleOption); err != nil {
			return fmt.Errorf("failed to register subsystem option: %w", err)
		}

		s.toggleValue = config.GetAsBool(s.ToggleOptionKey, false)
	} else {
		s.toggleValue = func() bool { return true }
	}

	mng.subsys[id] = s

	return nil
}

func (mng *Manager) shouldServeUpdates() bool {
	if !mng.immutable.IsSet() {
		// the manager must be marked as immutable before we
		// are going to handle any module changes.
		return false
	}
	if modules.IsShuttingDown() {
		// we don't care if we are shutting down anyway
		return false
	}
	return true
}

// CheckConfig checks subsystem configuration values and enables
// or disables subsystems and their dependencies as required.
func (mng *Manager) CheckConfig(ctx context.Context) error {
	// DEBUG SNIPPET
	// Slow-start for non-attributable performance issues.
	// You'll need the snippet in the modules too.
	// time.Sleep(11 * time.Second)
	// END DEBUG SNIPPET
	return mng.handleConfigChanges(ctx)
}

func (mng *Manager) handleModuleUpdate(m *modules.Module) {
	if !mng.shouldServeUpdates() {
		return
	}

	// Read lock is fine as the subsystems are write-locked on their own
	mng.l.RLock()
	defer mng.l.RUnlock()

	subsys, ms := mng.findParentSubsystem(m)
	if subsys == nil {
		// the updated module is not handled by any
		// subsystem. We're done here.
		return
	}

	subsys.Lock()
	defer subsys.Unlock()

	updated := compareAndUpdateStatus(m, ms)
	if updated {
		subsys.makeSummary()
	}

	if updated {
		mng.pushUpdate(subsys)
	}
}

func (mng *Manager) handleConfigChanges(_ context.Context) error {
	if !mng.shouldServeUpdates() {
		return nil
	}

	if mng.debounceUpdate.SetToIf(false, true) {
		time.Sleep(100 * time.Millisecond)
		mng.debounceUpdate.UnSet()
	} else {
		return nil
	}

	mng.l.RLock()
	defer mng.l.RUnlock()

	var changed bool
	for _, subsystem := range mng.subsys {
		if subsystem.module.SetEnabled(subsystem.toggleValue()) {
			changed = true
		}
	}
	if !changed {
		return nil
	}

	return modules.ManageModules()
}

func (mng *Manager) findParentSubsystem(m *modules.Module) (*Subsystem, *ModuleStatus) {
	for _, subsys := range mng.subsys {
		for _, ms := range subsys.Modules {
			if ms.Name == m.Name {
				return subsys, ms
			}
		}
	}
	return nil, nil
}

// helper type to sort a slice of []*Subsystem (casted as []record.Record) by
// id. Only use if it's guaranteed that all record.Records are *Subsystem.
// Otherwise Less() will panic.
type bySubsystemID []record.Record

func (sl bySubsystemID) Less(i, j int) bool { return sl[i].(*Subsystem).ID < sl[j].(*Subsystem).ID } //nolint:forcetypeassert // Can only be *Subsystem.
func (sl bySubsystemID) Swap(i, j int)      { sl[i], sl[j] = sl[j], sl[i] }
func (sl bySubsystemID) Len() int           { return len(sl) }
