package plugins

import (
	"fmt"

	"github.com/rs/zerolog"

	"github.com/m1k1o/neko/server/pkg/types"
)

type dependency struct {
	plugin    types.Plugin
	dependsOn []*dependency
	invoked   bool
	logger    zerolog.Logger
}

func (a *dependency) findPlugin(name string) (*dependency, bool) {
	if a == nil {
		return nil, false
	}

	if a.plugin.Name() == name {
		return a, true
	}

	for _, dep := range a.dependsOn {
		plug, ok := dep.findPlugin(name)
		if ok {
			return plug, true
		}
	}

	return nil, false
}

func (a *dependency) startPlugin(pm types.PluginManagers) error {
	if a.invoked {
		return nil
	}

	a.invoked = true

	for _, do := range a.dependsOn {
		if err := do.startPlugin(pm); err != nil {
			return fmt.Errorf("plugin's '%s' dependency: %w", a.plugin.Name(), err)
		}
	}

	err := a.plugin.Start(pm)
	if err != nil {
		return fmt.Errorf("plugin '%s' failed to start: %w", a.plugin.Name(), err)
	}

	a.logger.Info().Str("plugin", a.plugin.Name()).Msg("plugin started")
	return nil
}

type dependiencies struct {
	deps   map[string]*dependency
	logger zerolog.Logger
}

func (d *dependiencies) addPlugin(plugin types.Plugin) error {
	pluginName := plugin.Name()

	plug, ok := d.deps[pluginName]
	if !ok {
		plug = &dependency{}
	} else if plug.plugin != nil {
		return fmt.Errorf("plugin '%s' already added", pluginName)
	}

	plug.plugin = plugin
	plug.logger = d.logger
	d.deps[pluginName] = plug

	dplug, ok := plugin.(types.DependablePlugin)
	if !ok {
		return nil
	}

	for _, depName := range dplug.DependsOn() {
		dependsOn, ok := d.deps[depName]
		if !ok {
			dependsOn = &dependency{}
		} else if dependsOn.plugin != nil {
			// if there is a cyclical dependency, break it and return error
			if tdep, ok := dependsOn.findPlugin(pluginName); ok {
				dependsOn.dependsOn = nil
				delete(d.deps, pluginName)
				return fmt.Errorf("cyclical dependency detected: '%s' <-> '%s'", pluginName, tdep.plugin.Name())
			}
		}

		plug.dependsOn = append(plug.dependsOn, dependsOn)
		d.deps[depName] = dependsOn
	}

	return nil
}

func (d *dependiencies) findPlugin(name string) (*dependency, bool) {
	for _, dep := range d.deps {
		plug, ok := dep.findPlugin(name)
		if ok {
			return plug, true
		}
	}
	return nil, false
}

func (d *dependiencies) start(pm types.PluginManagers) error {
	for _, dep := range d.deps {
		if err := dep.startPlugin(pm); err != nil {
			return err
		}
	}
	return nil
}

func (d *dependiencies) forEach(f func(*dependency) error) error {
	for _, dep := range d.deps {
		if err := f(dep); err != nil {
			return err
		}
	}
	return nil
}

func (d *dependiencies) len() int {
	return len(d.deps)
}
