package spn

import (
	"context"
	"fmt"
	"sync/atomic"
	"time"

	"github.com/safing/portmaster/base/api"
	"github.com/safing/portmaster/base/config"
	"github.com/safing/portmaster/base/database/dbmodule"
	"github.com/safing/portmaster/base/metrics"
	"github.com/safing/portmaster/base/notifications"
	"github.com/safing/portmaster/base/rng"
	"github.com/safing/portmaster/base/runtime"
	"github.com/safing/portmaster/service/core"
	"github.com/safing/portmaster/service/core/base"
	"github.com/safing/portmaster/service/intel/filterlists"
	"github.com/safing/portmaster/service/intel/geoip"
	"github.com/safing/portmaster/service/mgr"
	"github.com/safing/portmaster/service/netenv"
	"github.com/safing/portmaster/service/updates"
	"github.com/safing/portmaster/spn/access"
	"github.com/safing/portmaster/spn/cabin"
	"github.com/safing/portmaster/spn/captain"
	"github.com/safing/portmaster/spn/crew"
	"github.com/safing/portmaster/spn/docks"
	"github.com/safing/portmaster/spn/navigator"
	"github.com/safing/portmaster/spn/patrol"
	"github.com/safing/portmaster/spn/ships"
	"github.com/safing/portmaster/spn/sluice"
	"github.com/safing/portmaster/spn/terminal"
)

// Instance is an instance of a Portmaster service.
type Instance struct {
	ctx          context.Context
	cancelCtx    context.CancelFunc
	serviceGroup *mgr.Group

	exitCode atomic.Int32

	base     *base.Base
	database *dbmodule.DBModule
	config   *config.Config
	api      *api.API
	metrics  *metrics.Metrics
	runtime  *runtime.Runtime
	rng      *rng.Rng

	core        *core.Core
	updates     *updates.Updates
	geoip       *geoip.GeoIP
	netenv      *netenv.NetEnv
	filterLists *filterlists.FilterLists

	access    *access.Access
	cabin     *cabin.Cabin
	navigator *navigator.Navigator
	captain   *captain.Captain
	crew      *crew.Crew
	docks     *docks.Docks
	patrol    *patrol.Patrol
	ships     *ships.Ships
	sluice    *sluice.SluiceModule
	terminal  *terminal.TerminalModule

	CommandLineOperation func() error
}

// New returns a new Portmaster service instance.
func New() (*Instance, error) {
	// Create instance to pass it to modules.
	instance := &Instance{}
	instance.ctx, instance.cancelCtx = context.WithCancel(context.Background())

	var err error

	// Base modules
	instance.base, err = base.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create base module: %w", err)
	}
	instance.database, err = dbmodule.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create database module: %w", err)
	}
	instance.config, err = config.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create config module: %w", err)
	}
	instance.api, err = api.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create api module: %w", err)
	}
	instance.metrics, err = metrics.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create metrics module: %w", err)
	}
	instance.runtime, err = runtime.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create runtime module: %w", err)
	}
	instance.rng, err = rng.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create rng module: %w", err)
	}

	// Service modules
	instance.core, err = core.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create core module: %w", err)
	}
	instance.updates, err = updates.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create updates module: %w", err)
	}
	instance.geoip, err = geoip.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create customlist module: %w", err)
	}
	instance.netenv, err = netenv.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create netenv module: %w", err)
	}
	instance.filterLists, err = filterlists.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create filterLists module: %w", err)
	}

	// SPN modules
	instance.access, err = access.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create access module: %w", err)
	}
	instance.cabin, err = cabin.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create cabin module: %w", err)
	}
	instance.navigator, err = navigator.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create navigator module: %w", err)
	}
	instance.crew, err = crew.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create crew module: %w", err)
	}
	instance.docks, err = docks.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create docks module: %w", err)
	}
	instance.patrol, err = patrol.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create patrol module: %w", err)
	}
	instance.ships, err = ships.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create ships module: %w", err)
	}
	instance.sluice, err = sluice.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create sluice module: %w", err)
	}
	instance.terminal, err = terminal.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create terminal module: %w", err)
	}
	instance.captain, err = captain.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create captain module: %w", err)
	}

	// Add all modules to instance group.
	instance.serviceGroup = mgr.NewGroup(
		instance.base,
		instance.database,
		instance.config,
		instance.api,
		instance.metrics,
		instance.runtime,
		instance.rng,

		instance.core,
		instance.updates,
		instance.geoip,
		instance.netenv,

		instance.access,
		instance.cabin,
		instance.navigator,
		instance.captain,
		instance.crew,
		instance.docks,
		instance.patrol,
		instance.ships,
		instance.sluice,
		instance.terminal,
	)

	return instance, nil
}

// AddModule validates the given module and adds it to the service group, if all requirements are met.
// All modules must be added before anything is done with the instance.
func (i *Instance) AddModule(m mgr.Module) {
	i.serviceGroup.Add(m)
}

// SleepyModule is an interface for modules that can enter some sort of sleep mode.
type SleepyModule interface {
	SetSleep(enabled bool)
}

// SetSleep sets sleep mode on all modules that satisfy the SleepyModule interface.
func (i *Instance) SetSleep(enabled bool) {
	for _, module := range i.serviceGroup.Modules() {
		if sm, ok := module.(SleepyModule); ok {
			sm.SetSleep(enabled)
		}
	}
}

// Database returns the database module.
func (i *Instance) Database() *dbmodule.DBModule {
	return i.database
}

// Config returns the config module.
func (i *Instance) Config() *config.Config {
	return i.config
}

// API returns the api module.
func (i *Instance) API() *api.API {
	return i.api
}

// Metrics returns the metrics module.
func (i *Instance) Metrics() *metrics.Metrics {
	return i.metrics
}

// Runtime returns the runtime module.
func (i *Instance) Runtime() *runtime.Runtime {
	return i.runtime
}

// Rng returns the rng module.
func (i *Instance) Rng() *rng.Rng {
	return i.rng
}

// Base returns the base module.
func (i *Instance) Base() *base.Base {
	return i.base
}

// Updates returns the updates module.
func (i *Instance) Updates() *updates.Updates {
	return i.updates
}

// GeoIP returns the geoip module.
func (i *Instance) GeoIP() *geoip.GeoIP {
	return i.geoip
}

// NetEnv returns the netenv module.
func (i *Instance) NetEnv() *netenv.NetEnv {
	return i.netenv
}

// Access returns the access module.
func (i *Instance) Access() *access.Access {
	return i.access
}

// Cabin returns the cabin module.
func (i *Instance) Cabin() *cabin.Cabin {
	return i.cabin
}

// Captain returns the captain module.
func (i *Instance) Captain() *captain.Captain {
	return i.captain
}

// Crew returns the crew module.
func (i *Instance) Crew() *crew.Crew {
	return i.crew
}

// Docks returns the crew module.
func (i *Instance) Docks() *docks.Docks {
	return i.docks
}

// Navigator returns the navigator module.
func (i *Instance) Navigator() *navigator.Navigator {
	return i.navigator
}

// Patrol returns the patrol module.
func (i *Instance) Patrol() *patrol.Patrol {
	return i.patrol
}

// Ships returns the ships module.
func (i *Instance) Ships() *ships.Ships {
	return i.ships
}

// Sluice returns the ships module.
func (i *Instance) Sluice() *sluice.SluiceModule {
	return i.sluice
}

// Terminal returns the terminal module.
func (i *Instance) Terminal() *terminal.TerminalModule {
	return i.terminal
}

// FilterLists returns the filterLists module.
func (i *Instance) FilterLists() *filterlists.FilterLists {
	return i.filterLists
}

// Core returns the core module.
func (i *Instance) Core() *core.Core {
	return i.core
}

// Events

// GetEventSPNConnected return the event manager for the SPN connected event.
func (i *Instance) GetEventSPNConnected() *mgr.EventMgr[struct{}] {
	return i.captain.EventSPNConnected
}

// Special functions

// SetCmdLineOperation sets a command line operation to be executed instead of starting the system. This is useful when functions need all modules to be prepared for a special operation.
func (i *Instance) SetCmdLineOperation(f func() error) {
	i.CommandLineOperation = f
}

// GetStates returns the current states of all group modules.
func (i *Instance) GetStates() []mgr.StateUpdate {
	return i.serviceGroup.GetStates()
}

// AddStatesCallback adds the given callback function to all group modules that
// expose a state manager at States().
func (i *Instance) AddStatesCallback(callbackName string, callback mgr.EventCallbackFunc[mgr.StateUpdate]) {
	i.serviceGroup.AddStatesCallback(callbackName, callback)
}

// Ready returns whether all modules in the main service module group have been started and are still running.
func (i *Instance) Ready() bool {
	return i.serviceGroup.Ready()
}

// Ctx returns the instance context.
// It is only canceled on shutdown.
func (i *Instance) Ctx() context.Context {
	return i.ctx
}

// Start starts the instance.
func (i *Instance) Start() error {
	return i.serviceGroup.Start()
}

// Stop stops the instance and cancels the instance context when done.
func (i *Instance) Stop() error {
	defer i.cancelCtx()
	return i.serviceGroup.Stop()
}

// RestartExitCode will instruct portmaster-start to restart the process immediately, potentially with a new version.
const RestartExitCode = 23

// Restart asynchronously restarts the instance.
// This only works if the underlying system/process supports this.
func (i *Instance) Restart() {
	// Send a restart event, give it 10ms extra to propagate.
	i.core.EventRestart.Submit(struct{}{})
	time.Sleep(10 * time.Millisecond)

	i.shutdown(RestartExitCode)
}

// Shutdown asynchronously stops the instance.
func (i *Instance) Shutdown() {
	// Send a shutdown event, give it 10ms extra to propagate.
	i.core.EventShutdown.Submit(struct{}{})
	time.Sleep(10 * time.Millisecond)

	i.shutdown(0)
}

func (i *Instance) shutdown(exitCode int) {
	// Set given exit code.
	i.exitCode.Store(int32(exitCode))

	m := mgr.New("instance")
	m.Go("shutdown", func(w *mgr.WorkerCtx) error {
		for {
			if err := i.Stop(); err != nil {
				w.Error("failed to shutdown", "err", err, "retry", "1s")
				time.Sleep(1 * time.Second)
			} else {
				return nil
			}
		}
	})
}

// Stopping returns whether the instance is shutting down.
func (i *Instance) Stopping() bool {
	return i.ctx.Err() == nil
}

// Stopped returns a channel that is triggered when the instance has shut down.
func (i *Instance) Stopped() <-chan struct{} {
	return i.ctx.Done()
}

// ExitCode returns the set exit code of the instance.
func (i *Instance) ExitCode() int {
	return int(i.exitCode.Load())
}

// SPNGroup fakes interface conformance.
// SPNGroup is only needed on SPN clients.
func (i *Instance) SPNGroup() *mgr.ExtendedGroup {
	return nil
}

// Unsupported Modules.

// Notifications returns nil.
func (i *Instance) Notifications() *notifications.Notifications { return nil }