package service

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/base/utils"
	"github.com/safing/portmaster/service/broadcasts"
	"github.com/safing/portmaster/service/compat"
	"github.com/safing/portmaster/service/control"
	"github.com/safing/portmaster/service/core"
	"github.com/safing/portmaster/service/core/base"
	"github.com/safing/portmaster/service/firewall"
	"github.com/safing/portmaster/service/firewall/interception"
	"github.com/safing/portmaster/service/firewall/interception/dnsmonitor"
	"github.com/safing/portmaster/service/integration"
	"github.com/safing/portmaster/service/intel/customlists"
	"github.com/safing/portmaster/service/intel/filterlists"
	"github.com/safing/portmaster/service/intel/geoip"
	"github.com/safing/portmaster/service/interop"
	"github.com/safing/portmaster/service/mgr"
	"github.com/safing/portmaster/service/nameserver"
	"github.com/safing/portmaster/service/netenv"
	"github.com/safing/portmaster/service/netquery"
	"github.com/safing/portmaster/service/network"
	"github.com/safing/portmaster/service/process"
	"github.com/safing/portmaster/service/profile"
	"github.com/safing/portmaster/service/resolver"
	"github.com/safing/portmaster/service/splittun"
	"github.com/safing/portmaster/service/status"
	"github.com/safing/portmaster/service/sync"
	"github.com/safing/portmaster/service/ui"
	"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/hub"
	"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

	shutdownCtx       context.Context
	cancelShutdownCtx context.CancelFunc

	serviceGroup             *mgr.Group
	serviceGroupInterception *mgr.GroupModule

	binDir  string
	dataDir string

	exitCode atomic.Int32

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

	core          *core.Core
	binaryUpdates *updates.Updater
	intelUpdates  *updates.Updater
	integration   *integration.OSIntegration
	geoip         *geoip.GeoIP
	netenv        *netenv.NetEnv
	ui            *ui.UI
	profile       *profile.ProfileModule
	network       *network.Network
	netquery      *netquery.NetQuery
	firewall      *firewall.Firewall
	filterLists   *filterlists.FilterLists
	interception  *interception.Interception
	dnsmonitor    *dnsmonitor.DNSMonitor
	customlist    *customlists.CustomList
	status        *status.Status
	broadcasts    *broadcasts.Broadcasts
	compat        *compat.Compat
	nameserver    *nameserver.NameServer
	process       *process.ProcessModule
	resolver      *resolver.ResolverModule
	sync          *sync.Sync
	control       *control.Control
	interop       *interop.Interoperability

	splittun *splittun.SplitTunModule

	access *access.Access

	// SPN modules
	SpnGroup  *mgr.ExtendedGroup
	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
	ShouldRestart        bool
}

// New returns a new Portmaster service instance.
func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
	// Initialize config.
	err := svcCfg.Init()
	if err != nil {
		return nil, fmt.Errorf("internal service config error: %w", err)
	}

	// Make sure data dir exists, so that child directories don't dictate the permissions.
	err = utils.EnsureDirectory(svcCfg.DataDir, utils.PublicReadExecPermission)
	if err != nil {
		return nil, fmt.Errorf("data directory %s is not accessible: %w", svcCfg.DataDir, err)
	}

	// Create instance to pass it to modules.
	instance := &Instance{
		binDir:  svcCfg.BinDir,
		dataDir: svcCfg.DataDir,
	}
	instance.ctx, instance.cancelCtx = context.WithCancel(context.Background())
	instance.shutdownCtx, instance.cancelShutdownCtx = context.WithCancel(context.Background())

	// 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.notifications, err = notifications.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
	binaryUpdateConfig, intelUpdateConfig, err := MakeUpdateConfigs(svcCfg)
	if err != nil {
		return instance, fmt.Errorf("create updates config: %w", err)
	}
	instance.binaryUpdates, err = updates.New(instance, "Binary Updater", *binaryUpdateConfig)
	if err != nil {
		return instance, fmt.Errorf("create updates module: %w", err)
	}
	instance.intelUpdates, err = updates.New(instance, "Intel Updater", *intelUpdateConfig)
	if err != nil {
		return instance, fmt.Errorf("create updates module: %w", err)
	}
	instance.core, err = core.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create core module: %w", err)
	}
	instance.integration, err = integration.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create integration 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.ui, err = ui.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create ui module: %w", err)
	}
	instance.profile, err = profile.NewModule(instance)
	if err != nil {
		return instance, fmt.Errorf("create profile module: %w", err)
	}
	instance.network, err = network.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create network module: %w", err)
	}
	instance.netquery, err = netquery.NewModule(instance)
	if err != nil {
		return instance, fmt.Errorf("create netquery module: %w", err)
	}
	instance.firewall, err = firewall.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create firewall module: %w", err)
	}
	instance.filterLists, err = filterlists.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create filterLists module: %w", err)
	}
	instance.interception, err = interception.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create interception module: %w", err)
	}
	instance.dnsmonitor, err = dnsmonitor.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create dns-listener module: %w", err)
	}
	instance.customlist, err = customlists.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create customlist module: %w", err)
	}

	instance.status, err = status.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create status module: %w", err)
	}
	instance.broadcasts, err = broadcasts.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create broadcasts module: %w", err)
	}
	instance.compat, err = compat.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create compat module: %w", err)
	}
	instance.nameserver, err = nameserver.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create nameserver module: %w", err)
	}
	instance.process, err = process.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create process module: %w", err)
	}
	instance.resolver, err = resolver.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create resolver module: %w", err)
	}
	instance.sync, err = sync.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create sync module: %w", err)
	}
	instance.control, err = control.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create control module: %w", err)
	}
	instance.interop, err = interop.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create interop module: %w", err)
	}
	instance.access, err = access.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create access module: %w", err)
	}

	instance.splittun, err = splittun.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create splittun module: %w", err)
	}

	// SPN modules
	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.captain, err = captain.New(instance)
	if err != nil {
		return instance, fmt.Errorf("create captain 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)
	}

	// Grouped interception modules that can be paused/resumed together.
	instance.serviceGroupInterception = mgr.NewGroupModule("Interception Group",
		instance.interception,
		instance.dnsmonitor,
		instance.compat)

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

		instance.core,
		instance.binaryUpdates,
		instance.intelUpdates,
		instance.integration,
		instance.geoip,
		instance.netenv,

		instance.process,
		instance.profile,
		instance.network,
		instance.netquery,
		instance.firewall,
		instance.nameserver,
		instance.resolver,
		instance.filterLists,
		instance.customlist,

		instance.splittun,

		instance.interop, // required to start before interception

		// Grouped pausable interception modules:
		// 		instance.interception,
		// 		instance.dnsmonitor,
		// 		instance.compat
		instance.serviceGroupInterception,

		instance.status,
		instance.broadcasts,
		instance.sync,
		instance.ui,
		instance.control,

		instance.access,
	)

	// SPN Group
	instance.SpnGroup = mgr.NewExtendedGroup(
		instance.cabin,
		instance.navigator,
		instance.captain,
		instance.crew,
		instance.docks,
		instance.patrol,
		instance.ships,
		instance.sluice,
		instance.terminal,
	)

	return instance, nil
}

// 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)
		}
	}
	for _, module := range i.SpnGroup.Modules() {
		if sm, ok := module.(SleepyModule); ok {
			sm.SetSleep(enabled)
		}
	}
}

// BinDir returns the directory for binaries.
// This directory may be read-only.
func (i *Instance) BinDir() string {
	return i.binDir
}

// DataDir returns the directory for variable data.
// This directory is expected to be read/writeable.
func (i *Instance) DataDir() string {
	return i.dataDir
}

// 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
}

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

// 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
}

// BinaryUpdates returns the updates module.
func (i *Instance) BinaryUpdates() *updates.Updater {
	return i.binaryUpdates
}

// GetBinaryUpdateFile returns the file path of a binary update file.
func (i *Instance) GetBinaryUpdateFile(name string) (path string, err error) {
	file, err := i.binaryUpdates.GetFile(name)
	if err != nil {
		return "", err
	}
	return file.Path(), nil
}

// IntelUpdates returns the updates module.
func (i *Instance) IntelUpdates() *updates.Updater {
	return i.intelUpdates
}

// OSIntegration returns the integration module.
func (i *Instance) OSIntegration() *integration.OSIntegration {
	return i.integration
}

// 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
}

// UI returns the ui module.
func (i *Instance) UI() *ui.UI {
	return i.ui
}

// Profile returns the profile module.
func (i *Instance) Profile() *profile.ProfileModule {
	return i.profile
}

// Firewall returns the firewall module.
func (i *Instance) Firewall() *firewall.Firewall {
	return i.firewall
}

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

// Interception returns the interception module.
func (i *Instance) Interception() *interception.Interception {
	return i.interception
}

// InterceptionGroup returns the grouped interception modules that can be paused together.
func (i *Instance) InterceptionGroup() *mgr.GroupModule {
	return i.serviceGroupInterception
}

// DNSMonitor returns the dns-listener module.
func (i *Instance) DNSMonitor() *dnsmonitor.DNSMonitor {
	return i.dnsmonitor
}

// CustomList returns the customlist module.
func (i *Instance) CustomList() *customlists.CustomList {
	return i.customlist
}

// Status returns the status module.
func (i *Instance) Status() *status.Status {
	return i.status
}

// Broadcasts returns the broadcast module.
func (i *Instance) Broadcasts() *broadcasts.Broadcasts {
	return i.broadcasts
}

// Compat returns the compat module.
func (i *Instance) Compat() *compat.Compat {
	return i.compat
}

// NameServer returns the nameserver module.
func (i *Instance) NameServer() *nameserver.NameServer {
	return i.nameserver
}

// NetQuery returns the netquery module.
func (i *Instance) NetQuery() *netquery.NetQuery {
	return i.netquery
}

// Network returns the network module.
func (i *Instance) Network() *network.Network {
	return i.network
}

// Process returns the process module.
func (i *Instance) Process() *process.ProcessModule {
	return i.process
}

// Resolver returns the resolver module.
func (i *Instance) Resolver() *resolver.ResolverModule {
	return i.resolver
}

// Sync returns the sync module.
func (i *Instance) Sync() *sync.Sync {
	return i.sync
}

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

// SPNGroup returns the group of all SPN modules.
func (i *Instance) SPNGroup() *mgr.ExtendedGroup {
	return i.SpnGroup
}

// Events

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

func (i *Instance) GetHookSPNConnecting() *mgr.HookMgr[hub.Announcement] {
	return i.captain.HookSPNConnecting
}

// 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 {
	mainStates := i.serviceGroup.GetStates()
	spnStates := i.SpnGroup.GetStates()

	updates := make([]mgr.StateUpdate, 0, len(mainStates)+len(spnStates))
	updates = append(updates, mainStates...)
	updates = append(updates, spnStates...)

	return updates
}

// 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)
	i.SpnGroup.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()
}

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

// Stop stops the instance modules.
func (i *Instance) Stop() error {
	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)

	// Set the restart flag and shutdown.
	i.ShouldRestart = true
	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) {
	// Only shutdown once.
	if i.IsShuttingDown() {
		return
	}

	// Cancel main  context.
	i.cancelCtx()

	// Set given exit code.
	i.exitCode.Store(int32(exitCode))

	// Start shutdown asynchronously in a separate manager.
	m := mgr.New("instance")
	m.Go("shutdown", func(w *mgr.WorkerCtx) error {
		// Stop all modules.
		if err := i.Stop(); err != nil {
			w.Error("failed to shutdown", "err", err)
		}

		// Cancel shutdown process context.
		i.cancelShutdownCtx()
		return nil
	})
}

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

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

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

// ShutdownCtx returns the instance shutdown context.
// It is canceled when shutdown is complete.
func (i *Instance) ShutdownCtx() context.Context {
	return i.shutdownCtx
}

// IsShutDown returns whether the instance has stopped.
func (i *Instance) IsShutDown() bool {
	return i.shutdownCtx.Err() != nil
}

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

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

// ShouldRestartIsSet returns whether the service/instance should be restarted.
func (i *Instance) ShouldRestartIsSet() bool {
	return i.ShouldRestart
}

// CommandLineOperationIsSet returns whether the command line option is set.
func (i *Instance) CommandLineOperationIsSet() bool {
	return i.CommandLineOperation != nil
}

// CommandLineOperationExecute executes the set command line option.
func (i *Instance) CommandLineOperationExecute() error {
	return i.CommandLineOperation()
}

// AddModule adds a module to the service group.
func (i *Instance) AddModule(m mgr.Module) {
	i.serviceGroup.Add(m)
}
