package profile

import (
	"errors"
	"fmt"
	"path/filepath"
	"sync/atomic"

	"github.com/safing/portmaster/base/config"
	"github.com/safing/portmaster/base/database"
	"github.com/safing/portmaster/base/database/migration"
	"github.com/safing/portmaster/base/log"
	"github.com/safing/portmaster/base/utils"
	_ "github.com/safing/portmaster/service/core/base"
	"github.com/safing/portmaster/service/mgr"
	"github.com/safing/portmaster/service/profile/binmeta"
)

var migrations = migration.New("core:migrations/profile")

// Events.
const (
	ConfigChangeEvent = "profile config change"
	DeletedEvent      = "profile deleted"
	MigratedEvent     = "profile migrated"
)

type ProfileModule struct {
	mgr      *mgr.Manager
	instance instance

	EventConfigChange *mgr.EventMgr[string]
	EventDelete       *mgr.EventMgr[string]
	EventMigrated     *mgr.EventMgr[[]string]

	states *mgr.StateMgr
}

func (pm *ProfileModule) Manager() *mgr.Manager {
	return pm.mgr
}

func (pm *ProfileModule) States() *mgr.StateMgr {
	return pm.states
}

func (pm *ProfileModule) Start() error {
	return start()
}

func (pm *ProfileModule) Stop() error {
	return stop()
}

func prep() error {
	if err := registerConfiguration(); err != nil {
		return err
	}

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

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

	// Setup icon storage location.
	databaseDir := filepath.Join(module.instance.DataDir(), "databases")
	// Ensure folder existents and permission
	err := utils.EnsureDirectory(databaseDir, utils.AdminOnlyExecPermission)
	if err != nil {
		return fmt.Errorf("failed to ensure directory existence %s: %w", databaseDir, err)
	}
	iconsDir := filepath.Join(databaseDir, "icons")
	err = utils.EnsureDirectory(iconsDir, utils.AdminOnlyExecPermission)
	if err != nil {
		return fmt.Errorf("failed to ensure directory existence %s: %w", iconsDir, err)
	}

	binmeta.ProfileIconStoragePath = iconsDir

	return nil
}

func start() error {
	if err := loadProfilesMetadata(); err != nil {
		if !errors.Is(err, database.ErrNotFound) {
			log.Warningf("profile: failed to load profiles metadata, falling back to empty state: %s", err)
		}
		meta = &ProfilesMetadata{}
	}
	meta.check()

	if err := migrations.Migrate(module.mgr.Ctx()); err != nil {
		log.Errorf("profile: migrations failed: %s", err)
	}

	err := registerValidationDBHook()
	if err != nil {
		return err
	}

	err = registerRevisionProvider()
	if err != nil {
		return err
	}

	err = startProfileUpdateChecker()
	if err != nil {
		return err
	}

	module.mgr.Go("clean active profiles", cleanActiveProfiles)

	// Register config callback when starting, as it depends on the updates module,
	// but the config system will already submit events earlier.
	if err := registerGlobalConfigProfileUpdater(); err != nil {
		return err
	}

	err = updateGlobalConfigProfile(module.mgr.Ctx())
	if err != nil {
		log.Warningf("profile: error during loading global profile from configuration: %s", err)
	}

	return nil
}

func stop() error {
	return meta.Save()
}

var (
	module     *ProfileModule
	shimLoaded atomic.Bool
)

func NewModule(instance instance) (*ProfileModule, error) {
	if !shimLoaded.CompareAndSwap(false, true) {
		return nil, errors.New("only one instance allowed")
	}
	m := mgr.New("Profile")
	module = &ProfileModule{
		mgr:      m,
		instance: instance,

		EventConfigChange: mgr.NewEventMgr[string](ConfigChangeEvent, m),
		EventDelete:       mgr.NewEventMgr[string](DeletedEvent, m),
		EventMigrated:     mgr.NewEventMgr[[]string](MigratedEvent, m),

		states: mgr.NewStateMgr(m),
	}

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

	return module, nil
}

type instance interface {
	DataDir() string
	Config() *config.Config
}
