package database

import (
	"errors"
	"fmt"
	"sync"

	"github.com/safing/portmaster/base/database/storage"
)

// StorageTypeInjected is the type of injected databases.
const StorageTypeInjected = "injected"

var (
	controllers     = make(map[string]*Controller)
	controllersLock sync.RWMutex
)

func getController(name string) (*Controller, error) {
	if !initialized.IsSet() {
		return nil, errors.New("database not initialized")
	}

	// return database if already started
	controllersLock.RLock()
	controller, ok := controllers[name]
	controllersLock.RUnlock()
	if ok {
		return controller, nil
	}

	controllersLock.Lock()
	defer controllersLock.Unlock()

	if shuttingDown.IsSet() {
		return nil, ErrShuttingDown
	}

	// get db registration
	registeredDB, err := getDatabase(name)
	if err != nil {
		return nil, fmt.Errorf("could not start database %s: %w", name, err)
	}

	// Check if database is injected.
	if registeredDB.StorageType == StorageTypeInjected {
		return nil, fmt.Errorf("database storage is not injected")
	}

	// get location
	dbLocation, err := getLocation(name, registeredDB.StorageType)
	if err != nil {
		return nil, fmt.Errorf("could not start database %s (type %s): %w", name, registeredDB.StorageType, err)
	}

	// start database
	storageInt, err := storage.StartDatabase(name, registeredDB.StorageType, dbLocation)
	if err != nil {
		return nil, fmt.Errorf("could not start database %s (type %s): %w", name, registeredDB.StorageType, err)
	}

	controller = newController(registeredDB, storageInt, registeredDB.ShadowDelete)
	controllers[name] = controller
	return controller, nil
}

// InjectDatabase injects an already running database into the system.
func InjectDatabase(name string, storageInt storage.Interface) (*Controller, error) {
	controllersLock.Lock()
	defer controllersLock.Unlock()

	if shuttingDown.IsSet() {
		return nil, ErrShuttingDown
	}

	_, ok := controllers[name]
	if ok {
		return nil, fmt.Errorf(`database "%s" already loaded`, name)
	}

	registryLock.Lock()
	defer registryLock.Unlock()

	// check if database is registered
	registeredDB, ok := registry[name]
	if !ok {
		return nil, fmt.Errorf("database %q not registered", name)
	}
	if registeredDB.StorageType != StorageTypeInjected {
		return nil, fmt.Errorf("database not of type %q", StorageTypeInjected)
	}

	controller := newController(registeredDB, storageInt, false)
	controllers[name] = controller
	return controller, nil
}

// Withdraw withdraws an injected database, but leaves the database registered.
func (c *Controller) Withdraw() {
	if c != nil && c.Injected() {
		controllersLock.Lock()
		defer controllersLock.Unlock()

		delete(controllers, c.database.Name)
	}
}