package access

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

	"github.com/tevino/abool"

	"github.com/safing/portmaster/base/config"
	"github.com/safing/portmaster/base/log"
	"github.com/safing/portmaster/service/mgr"
	"github.com/safing/portmaster/spn/access/account"
	"github.com/safing/portmaster/spn/access/token"
	"github.com/safing/portmaster/spn/conf"
)

type Access struct {
	mgr      *mgr.Manager
	instance instance

	updateAccountWorkerMgr *mgr.WorkerMgr

	EventAccountUpdate *mgr.EventMgr[struct{}]
}

func (a *Access) Manager() *mgr.Manager {
	return a.mgr
}

func (a *Access) Start() error {
	return start()
}

func (a *Access) Stop() error {
	return stop()
}

var (
	module     *Access
	shimLoaded atomic.Bool

	tokenIssuerIsFailing     = abool.New()
	tokenIssuerRetryDuration = 10 * time.Minute

	// AccountUpdateEvent is fired when the account has changed in any way.
	AccountUpdateEvent = "account update"
)

// Errors.
var (
	ErrDeviceIsLocked       = errors.New("device is locked")
	ErrDeviceLimitReached   = errors.New("device limit reached")
	ErrFallbackNotAvailable = errors.New("fallback tokens not available, token issuer is online")
	ErrInvalidCredentials   = errors.New("invalid credentials")
	ErrMayNotUseSPN         = errors.New("may not use SPN")
	ErrNotLoggedIn          = errors.New("not logged in")
)

func prep() error {
	// Register API handlers.
	if conf.Integrated() {
		err := registerAPIEndpoints()
		if err != nil {
			return err
		}
	}

	return nil
}

func start() error {
	// Initialize zones.
	if err := InitializeZones(); err != nil {
		return err
	}

	if conf.Integrated() {
		// Add config listener to enable/disable SPN.
		module.instance.Config().EventConfigChange.AddCallback("spn enable check", func(wc *mgr.WorkerCtx, s struct{}) (bool, error) {
			// Do not do anything when we are shutting down.
			if module.instance.IsShuttingDown() {
				return true, nil
			}

			enabled := config.GetAsBool("spn/enable", false)
			if enabled() {
				log.Info("spn: starting SPN")
				module.mgr.Go("ensure SPN is started", module.instance.SPNGroup().EnsureStartedWorker)
			} else {
				log.Info("spn: stopping SPN")
				module.mgr.Go("ensure SPN is stopped", module.instance.SPNGroup().EnsureStoppedWorker)
			}
			return false, nil
		})

		// Load tokens from database.
		loadTokens()

		// Check if we need to enable SPN now.
		enabled := config.GetAsBool("spn/enable", false)
		if enabled() {
			module.mgr.Go("ensure SPN is started", module.instance.SPNGroup().EnsureStartedWorker)
		}

		// Register new task.
		module.updateAccountWorkerMgr.Delay(1 * time.Minute)
	}

	return nil
}

func stop() error {
	if conf.Integrated() {
		// Make sure SPN is stopped before we proceed.
		err := module.mgr.Do("ensure SPN is shut down", module.instance.SPNGroup().EnsureStoppedWorker)
		if err != nil {
			log.Errorf("access: stop SPN: %s", err)
		}

		// Store tokens to database.
		storeTokens()
	}

	// Reset zones.
	token.ResetRegistry()

	return nil
}

// UpdateAccount updates the user account and fetches new tokens, if needed.
func UpdateAccount(_ *mgr.WorkerCtx) error {
	// Schedule next call - this will change if other conditions are met bellow.
	module.updateAccountWorkerMgr.Delay(24 * time.Hour)

	// Retry sooner if the token issuer is failing.
	defer func() {
		if tokenIssuerIsFailing.IsSet() {
			module.updateAccountWorkerMgr.Delay(tokenIssuerRetryDuration)
		}
	}()

	// Get current user.
	u, err := GetUser()
	if err == nil {
		// Do not update if we just updated.
		if time.Since(time.Unix(u.Meta().Modified, 0)) < 2*time.Minute {
			return nil
		}
	}

	u, _, err = UpdateUser()
	if err != nil {
		return fmt.Errorf("failed to update user profile: %w", err)
	}

	err = UpdateTokens()
	if err != nil {
		return fmt.Errorf("failed to get tokens: %w", err)
	}

	// Schedule next check.
	switch {
	case u == nil: // No user.
	case u.Subscription == nil: // No subscription.
	case u.Subscription.EndsAt == nil: // Subscription not active

	case time.Until(*u.Subscription.EndsAt) < 24*time.Hour &&
		time.Since(*u.Subscription.EndsAt) < 24*time.Hour:
		// Update account every hour for 24h hours before and after the subscription ends.
		module.updateAccountWorkerMgr.Delay(1 * time.Hour)

	case u.Subscription.NextBillingDate == nil: // No auto-subscription.

	case time.Until(*u.Subscription.NextBillingDate) < 24*time.Hour &&
		time.Since(*u.Subscription.NextBillingDate) < 24*time.Hour:
		// Update account every hour 24h hours before and after the next billing date.
		module.updateAccountWorkerMgr.Delay(1 * time.Hour)
	}

	return nil
}

func enableSPN() {
	err := config.SetConfigOption("spn/enable", true)
	if err != nil {
		log.Warningf("spn/access: failed to enable the SPN during login: %s", err)
	}
}

func disableSPN() {
	err := config.SetConfigOption("spn/enable", false)
	if err != nil {
		log.Warningf("spn/access: failed to disable the SPN during logout: %s", err)
	}
}

// TokenIssuerIsFailing returns whether token issuing is currently failing.
func TokenIssuerIsFailing() bool {
	return tokenIssuerIsFailing.IsSet()
}

func tokenIssuerFailed() {
	if !tokenIssuerIsFailing.SetToIf(false, true) {
		return
	}

	module.updateAccountWorkerMgr.Delay(tokenIssuerRetryDuration)
}

// IsLoggedIn returns whether a User is currently logged in.
func (user *UserRecord) IsLoggedIn() bool {
	user.Lock()
	defer user.Unlock()

	switch user.State {
	case account.UserStateNone, account.UserStateLoggedOut:
		return false
	default:
		return true
	}
}

// MayUseTheSPN returns whether the currently logged in User may use the SPN.
func (user *UserRecord) MayUseTheSPN() bool {
	user.Lock()
	defer user.Unlock()

	return user.User.MayUseSPN()
}

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

	m := mgr.New("Access")
	module = &Access{
		mgr:      m,
		instance: instance,

		EventAccountUpdate:     mgr.NewEventMgr[struct{}](AccountUpdateEvent, m),
		updateAccountWorkerMgr: m.NewWorkerMgr("update account", UpdateAccount, nil),
	}

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

	return module, nil
}

type instance interface {
	Config() *config.Config
	SPNGroup() *mgr.ExtendedGroup
	IsShuttingDown() bool
}
