package captain

import (
	"errors"
	"fmt"
	"net"
	"net/http"
	"sync/atomic"
	"time"

	"github.com/safing/portmaster/base/api"
	"github.com/safing/portmaster/base/config"
	"github.com/safing/portmaster/base/log"
	"github.com/safing/portmaster/base/rng"
	"github.com/safing/portmaster/service/mgr"
	"github.com/safing/portmaster/service/netenv"
	"github.com/safing/portmaster/service/network/netutils"
	"github.com/safing/portmaster/service/updates"
	"github.com/safing/portmaster/spn/conf"
	"github.com/safing/portmaster/spn/crew"
	"github.com/safing/portmaster/spn/navigator"
	"github.com/safing/portmaster/spn/patrol"
	"github.com/safing/portmaster/spn/ships"
)

// SPNConnectedEvent is the name of the event that is fired when the SPN has connected and is ready.
const SPNConnectedEvent = "spn connect"

// Captain is the main module of the SPN.
type Captain struct {
	mgr      *mgr.Manager
	instance instance

	healthCheckTicker *mgr.SleepyTicker

	publicIdentityUpdater *mgr.WorkerMgr
	statusUpdater         *mgr.WorkerMgr

	states            *mgr.StateMgr
	EventSPNConnected *mgr.EventMgr[struct{}]
}

// Manager returns the module manager.
func (c *Captain) Manager() *mgr.Manager {
	return c.mgr
}

// States returns the module states.
func (c *Captain) States() *mgr.StateMgr {
	return c.states
}

// Start starts the module.
func (c *Captain) Start() error {
	return start()
}

// Stop stops the module.
func (c *Captain) Stop() error {
	return stop()
}

// SetSleep sets the sleep mode of the module.
func (c *Captain) SetSleep(enabled bool) {
	if c.healthCheckTicker != nil {
		c.healthCheckTicker.SetSleep(enabled)
	}
}

func (c *Captain) prep() error {
	// Check if we can parse the bootstrap hub flag.
	if err := prepBootstrapHubFlag(); err != nil {
		return err
	}

	// Register SPN status provider.
	if err := registerSPNStatusProvider(); err != nil {
		return err
	}

	// Register API endpoints.
	if err := registerAPIEndpoints(); err != nil {
		return err
	}

	if conf.PublicHub() {
		// Register API authenticator.
		if err := api.SetAuthenticator(apiAuthenticator); err != nil {
			return err
		}

		c.instance.Patrol().EventChangeSignal.AddCallback(
			"trigger hub status maintenance",
			func(_ *mgr.WorkerCtx, _ struct{}) (bool, error) {
				TriggerHubStatusMaintenance()
				return false, nil
			},
		)
	}

	return prepConfig()
}

func start() error {
	maskingBytes, err := rng.Bytes(16)
	if err != nil {
		return fmt.Errorf("failed to get random bytes for masking: %w", err)
	}
	ships.EnableMasking(maskingBytes)

	// Initialize identity and piers.
	if conf.PublicHub() {
		// Load identity.
		if err := loadPublicIdentity(); err != nil {
			return fmt.Errorf("load public identity: %w", err)
		}

		// Check if any networks are configured.
		if !conf.HubHasIPv4() && !conf.HubHasIPv6() {
			return errors.New("no IP addresses for Hub configured (or detected)")
		}

		// Start management of identity and piers.
		if err := prepPublicIdentityMgmt(); err != nil {
			return err
		}
		// Set ID to display on http info page.
		ships.DisplayHubID = publicIdentity.ID
		// Start listeners.
		if err := startPiers(); err != nil {
			return err
		}

		// Enable connect operation.
		crew.EnableConnecting(publicIdentity.Hub)
	}

	// Initialize intel.
	module.mgr.Go("start", func(wc *mgr.WorkerCtx) error {
		if err := registerIntelUpdateHook(); err != nil {
			return err
		}
		if err := updateSPNIntel(module.mgr.Ctx(), nil); err != nil {
			log.Errorf("spn/captain: failed to update SPN intel: %s", err)
		}
		return nil
	})

	// Subscribe to updates of cranes.
	startDockHooks()

	// bootstrapping
	if err := processBootstrapHubFlag(); err != nil {
		return err
	}
	if err := processBootstrapFileFlag(); err != nil {
		return err
	}

	// network optimizer
	if conf.PublicHub() {
		module.mgr.Delay("optimize network delay", 15*time.Second, optimizeNetwork).Repeat(1 * time.Minute)
	}

	// client + home hub manager
	if conf.Client() {
		module.mgr.Go("client manager", clientManager)

		// Reset failing hubs when the network changes while not connected.
		module.instance.NetEnv().EventNetworkChange.AddCallback("reset failing hubs", func(_ *mgr.WorkerCtx, _ struct{}) (bool, error) {
			if ready.IsNotSet() {
				navigator.Main.ResetFailingStates()
			}
			return false, nil
		})
	}

	return nil
}

func stop() error {
	// Reset intel resource so that it is loaded again when starting.
	resetSPNIntel()

	// Unregister crane update hook.
	stopDockHooks()

	// Send shutdown status message.
	if conf.PublicHub() {
		publishShutdownStatus()
		stopPiers()
	}

	return nil
}

// apiAuthenticator grants User permissions for local API requests.
func apiAuthenticator(r *http.Request, s *http.Server) (*api.AuthToken, error) {
	// Get remote IP.
	host, _, err := net.SplitHostPort(r.RemoteAddr)
	if err != nil {
		return nil, fmt.Errorf("failed to split host/port: %w", err)
	}
	remoteIP := net.ParseIP(host)
	if remoteIP == nil {
		return nil, fmt.Errorf("failed to parse remote address %s", host)
	}

	if !netutils.GetIPScope(remoteIP).IsLocalhost() {
		return nil, api.ErrAPIAccessDeniedMessage
	}

	return &api.AuthToken{
		Read:  api.PermitUser,
		Write: api.PermitUser,
	}, nil
}

var (
	module     *Captain
	shimLoaded atomic.Bool
)

// New returns a new Captain module.
func New(instance instance) (*Captain, error) {
	if !shimLoaded.CompareAndSwap(false, true) {
		return nil, errors.New("only one instance allowed")
	}
	m := mgr.New("Captain")
	module = &Captain{
		mgr:      m,
		instance: instance,

		states:            mgr.NewStateMgr(m),
		EventSPNConnected: mgr.NewEventMgr[struct{}](SPNConnectedEvent, m),

		publicIdentityUpdater: m.NewWorkerMgr("maintain public identity", maintainPublicIdentity, nil),
		statusUpdater:         m.NewWorkerMgr("maintain public status", maintainPublicStatus, nil),
	}

	if err := module.prep(); err != nil {
		return nil, err
	}

	return module, nil
}

type instance interface {
	NetEnv() *netenv.NetEnv
	Patrol() *patrol.Patrol
	Config() *config.Config
	IntelUpdates() *updates.Updater
	SPNGroup() *mgr.ExtendedGroup
}
