package io.xpipe.app.prefs;

import io.xpipe.app.core.*;
import io.xpipe.app.core.mode.AppOperationMode;
import io.xpipe.app.ext.PrefsHandler;
import io.xpipe.app.ext.PrefsProvider;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.icon.SystemIconSource;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.*;
import io.xpipe.app.process.LocalShell;
import io.xpipe.app.process.ShellDialect;
import io.xpipe.app.process.ShellScript;
import io.xpipe.app.pwman.PasswordManager;
import io.xpipe.app.rdp.ExternalRdpClient;
import io.xpipe.app.spice.ExternalSpiceClient;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStorageUserHandler;
import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.terminal.TerminalMultiplexer;
import io.xpipe.app.terminal.TerminalPrompt;
import io.xpipe.app.terminal.TerminalSplitStrategy;
import io.xpipe.app.update.AppDistributionType;
import io.xpipe.app.util.*;
import io.xpipe.app.vnc.ExternalVncClient;
import io.xpipe.app.vnc.InternalVncClient;
import io.xpipe.app.vnc.VncCategory;
import io.xpipe.app.util.FilePath;
import io.xpipe.app.util.OsType;

import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableDoubleValue;
import javafx.beans.value.ObservableStringValue;
import javafx.beans.value.ObservableValue;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.SimpleType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import lombok.*;

import java.nio.file.Files;
import java.time.Duration;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.*;

public final class AppPrefs {

    private static AppPrefs INSTANCE;
    private final List<Mapping> mapping = new ArrayList<>();

    public static void initLocal() {
        INSTANCE = new AppPrefs();
        PrefsProvider.getAll().forEach(prov -> prov.addPrefs(INSTANCE.extensionHandler));
        INSTANCE.loadLocal();
        INSTANCE.vaultStorageHandler =
                new AppPrefsStorageHandler(DataStorage.getStorageDirectory().resolve("preferences.json"));
        INSTANCE.fixLocalValues();
    }

    public static void initSynced() throws Exception {
        INSTANCE.loadSharedRemote();
        INSTANCE.encryptAllVaultData.addListener((observableValue, aBoolean, t1) -> {
            if (DataStorage.get() != null) {
                DataStorage.get().forceRewrite();
            }
        });
    }

    public static void initStorage() {
        INSTANCE.vaultAuthentication.set(DataStorageUserHandler.getInstance().getVaultAuthenticationType());
    }

    public static void reset() {
        INSTANCE.save();

        // Keep instance as we might need some values on shutdown, e.g. on update with terminals
        // INSTANCE = null;
    }

    public static AppPrefs get() {
        return INSTANCE;
    }

    @Getter
    private final BooleanProperty requiresRestart = new GlobalBooleanProperty(false);

    final BooleanProperty disableHardwareAcceleration = map(Mapping.builder()
            .property(new GlobalBooleanProperty(false))
            .key("disableHardwareAcceleration")
            .valueClass(Boolean.class)
            .requiresRestart(true)
            .build());
    final BooleanProperty preferMonochromeIcons = map(Mapping.builder()
            .property(new GlobalBooleanProperty(false))
            .key("preferMonochromeIcons")
            .valueClass(Boolean.class)
            .requiresRestart(false)
            .build());
    final BooleanProperty useExternalNetcatForProxies = map(Mapping.builder()
            .property(new GlobalBooleanProperty(false))
            .key("useExternalNetcatForProxies")
            .valueClass(Boolean.class)
            .requiresRestart(false)
            .build());
    final BooleanProperty pinLocalMachineOnStartup = map(Mapping.builder()
            .property(new GlobalBooleanProperty(false))
            .key("pinLocalMachineOnStartup")
            .valueClass(Boolean.class)
            .requiresRestart(true)
            .build());
    final BooleanProperty hideVaultEntryNames = map(Mapping.builder()
            .property(new GlobalBooleanProperty(false))
            .vaultSpecific(true)
            .key("hideVaultEntryNames")
            .valueClass(Boolean.class)
            .requiresRestart(false)
            .build());
    final BooleanProperty enableHttpApi = map(Mapping.builder()
            .property(new GlobalBooleanProperty(false))
            .key("enableHttpApi")
            .valueClass(Boolean.class)
            .requiresRestart(false)
            .documentationLink(DocumentationLink.API)
            .build());
    final BooleanProperty enableMcpServer = map(Mapping.builder()
            .property(new GlobalBooleanProperty(false))
            .key("enableMcpServer")
            .valueClass(Boolean.class)
            .requiresRestart(false)
            .documentationLink(DocumentationLink.MCP)
            .build());
    final StringProperty mcpAdditionalContext = map(Mapping.builder()
            .property(new GlobalStringProperty(null))
            .key("mcpAdditionalContext")
            .valueClass(String.class)
            .requiresRestart(true)
            .build());
    final BooleanProperty dontAutomaticallyStartVmSshServer =
            mapVaultShared(new GlobalBooleanProperty(false), "dontAutomaticallyStartVmSshServer", Boolean.class, false);
    final BooleanProperty dontAcceptNewHostKeys =
            mapVaultShared(new GlobalBooleanProperty(false), "dontAcceptNewHostKeys", Boolean.class, false);
    public final BooleanProperty performanceMode = map(Mapping.builder()
            .property(new GlobalBooleanProperty())
            .key("performanceMode")
            .valueClass(Boolean.class)
            .build());
    final BooleanProperty limitedTouchscreenMode = map(Mapping.builder()
            .property(new GlobalBooleanProperty())
            .key("limitedTouchscreenMode")
            .valueClass(Boolean.class)
            .requiresRestart(true)
            .build());
    public final ObjectProperty<AppTheme.Theme> theme = map(Mapping.builder()
            .property(new GlobalObjectProperty<>())
            .key("theme")
            .valueClass(AppTheme.Theme.class)
            .build());
    final BooleanProperty useSystemFont = map(Mapping.builder()
            .property(new GlobalBooleanProperty(OsType.ofLocal() != OsType.MACOS))
            .key("useSystemFont")
            .valueClass(Boolean.class)
            .build());
    final Property<Integer> uiScale = map(Mapping.builder()
            .property(new GlobalObjectProperty<>())
            .key("uiScale")
            .valueClass(Integer.class)
            .requiresRestart(true)
            .build());
    final BooleanProperty saveWindowLocation = map(Mapping.builder()
            .property(new GlobalBooleanProperty(true))
            .key("saveWindowLocation")
            .valueClass(Boolean.class)
            .requiresRestart(false)
            .build());
    final Property<Integer> backgroundSessionInactivityTimeout = map(Mapping.builder()
            .property(new GlobalObjectProperty<>())
            .key("backgroundSessionInactivityTimeout")
            .valueClass(Integer.class)
            .requiresRestart(false)
            .vaultSpecific(true)
            .build());
    final BooleanProperty preferTerminalTabs = map(Mapping.builder()
            .property(new GlobalBooleanProperty(true))
            .key("preferTerminalTabs")
            .valueClass(Boolean.class)
            .requiresRestart(false)
            .build());
    final ObjectProperty<ExternalTerminalType> terminalType = map(Mapping.builder()
            .property(new GlobalObjectProperty<>())
            .key("terminalType")
            .valueClass(ExternalTerminalType.class)
            .requiresRestart(false)
            .documentationLink(DocumentationLink.TERMINAL)
            .build());
    final ObjectProperty<ExternalRdpClient> rdpClientType = map(Mapping.builder()
            .property(new GlobalObjectProperty<>())
            .key("rdpClientType")
            .valueClass(ExternalRdpClient.class)
            .requiresRestart(false)
            .documentationLink(DocumentationLink.RDP)
            .build());
    final StringProperty notesTemplate = new GlobalStringProperty(null);
    final DoubleProperty windowOpacity = map(Mapping.builder()
            .property(new GlobalDoubleProperty(1.0))
            .key("windowOpacity")
            .valueClass(Double.class)
            .requiresRestart(false)
            .build());
    final StringProperty customTerminalCommand = map(Mapping.builder()
            .property(new GlobalStringProperty(null))
            .key("customTerminalCommand")
            .valueClass(String.class)
            .requiresRestart(false)
            .build());
    final BooleanProperty clearTerminalOnInit = map(Mapping.builder()
            .property(new GlobalBooleanProperty(true))
            .key("clearTerminalOnInit")
            .valueClass(Boolean.class)
            .requiresRestart(false)
            .build());
    final Property<List<SystemIconSource>> iconSources = map(Mapping.builder()
            .property(new GlobalObjectProperty<>(new ArrayList<>()))
            .key("iconSources")
            .valueType(TypeFactory.defaultInstance().constructType(new TypeReference<List<SystemIconSource>>() {}))
            .vaultSpecific(true)
            .build());
    public final BooleanProperty disableCertutilUse = map(Mapping.builder()
            .property(new GlobalBooleanProperty(false))
            .key("disableCertutilUse")
            .valueClass(Boolean.class)
            .build());
    public final BooleanProperty useLocalFallbackShell = map(Mapping.builder()
            .property(new GlobalBooleanProperty(false))
            .key("useLocalFallbackShell")
            .valueClass(Boolean.class)
            .requiresRestart(true)
            .build());
    final Property<ShellDialect> localShellDialect = map(Mapping.builder()
            .property(new GlobalObjectProperty<>(
                    ProcessControlProvider.get().getAvailableLocalDialects().getFirst()))
            .key("localShellDialect")
            .valueClass(ShellDialect.class)
            .vaultSpecific(false)
            .requiresRestart(true)
            .build());

    public final BooleanProperty disableTerminalRemotePasswordPreparation = mapVaultShared(
            new GlobalBooleanProperty(false), "disableTerminalRemotePasswordPreparation", Boolean.class, false);
    public final Property<Boolean> alwaysConfirmElevation =
            mapVaultShared(new GlobalObjectProperty<>(false), "alwaysConfirmElevation", Boolean.class, false);
    public final BooleanProperty focusWindowOnNotifications = map(Mapping.builder()
            .property(new GlobalBooleanProperty(true))
            .key("focusWindowOnNotifications")
            .valueClass(Boolean.class)
            .build());
    public final BooleanProperty dontCachePasswords =
            mapVaultShared(new GlobalBooleanProperty(false), "dontCachePasswords", Boolean.class, false);
    public final Property<ExternalVncClient> vncClient = map(Mapping.builder()
            .property(new GlobalObjectProperty<>(InternalVncClient.builder().build()))
            .key("vncClient")
            .valueClass(ExternalVncClient.class)
            .documentationLink(DocumentationLink.VNC)
            .build());
    public final Property<ExternalSpiceClient> spiceClient = map(Mapping.builder()
            .property(new GlobalObjectProperty<>())
            .key("spiceClient")
            .valueClass(ExternalSpiceClient.class)
            .build());
    final Property<PasswordManager> passwordManager = map(Mapping.builder()
            .property(new GlobalObjectProperty<>())
            .key("passwordManager")
            .valueClass(PasswordManager.class)
            .log(false)
            .documentationLink(DocumentationLink.PASSWORD_MANAGER)
            .build());
    final Property<ShellScript> terminalInitScript = map(Mapping.builder()
            .property(new GlobalObjectProperty<>(null))
            .key("terminalInitScript")
            .valueClass(ShellScript.class)
            .log(false)
            .build());
    final ObjectProperty<HttpProxy> httpProxy = map(Mapping.builder()
            .property(new GlobalObjectProperty<>())
            .key("httpProxy")
            .valueClass(HttpProxy.class)
            .requiresRestart(true)
            .build());
    final Property<UUID> terminalProxy = map(Mapping.builder()
            .property(new GlobalObjectProperty<>())
            .key("terminalProxy")
            .valueClass(UUID.class)
            .requiresRestart(false)
            .build());
    final ObjectProperty<TerminalMultiplexer> terminalMultiplexer = map(Mapping.builder()
            .property(new GlobalObjectProperty<>(null))
            .key("terminalMultiplexer")
            .valueClass(TerminalMultiplexer.class)
            .log(false)
            .documentationLink(DocumentationLink.TERMINAL_MULTIPLEXER)
            .build());
    final Property<Boolean> terminalAlwaysPauseOnExit = map(Mapping.builder()
            .property(new GlobalBooleanProperty(true))
            .key("terminalAlwaysPauseOnExit")
            .valueClass(Boolean.class)
            .build());
    final Property<TerminalSplitStrategy> terminalSplitStrategy = map(Mapping.builder()
            .property(new GlobalObjectProperty<>(TerminalSplitStrategy.BALANCED))
            .key("terminalSplitStrategy")
            .valueClass(TerminalSplitStrategy.class)
            .build());
    final Property<TerminalPrompt> terminalPrompt = map(Mapping.builder()
            .property(new GlobalObjectProperty<>(null))
            .key("terminalPrompt")
            .valueClass(TerminalPrompt.class)
            .log(false)
            .documentationLink(DocumentationLink.TERMINAL_PROMPT)
            .build());
    final ObjectProperty<VaultAuthentication> vaultAuthentication = new GlobalObjectProperty<>();

    final ObjectProperty<StartupBehaviour> startupBehaviour = map(Mapping.builder()
            .property(new GlobalObjectProperty<>(StartupBehaviour.GUI))
            .key("startupBehaviour")
            .valueClass(StartupBehaviour.class)
            .requiresRestart(true)
            .build());
    public final BooleanProperty enableGitStorage = map(Mapping.builder()
            .property(new GlobalBooleanProperty(false))
            .key("enableGitStorage")
            .valueClass(Boolean.class)
            .requiresRestart(true)
            .documentationLink(DocumentationLink.SYNC)
            .build());
    final StringProperty storageGitRemote = map(Mapping.builder()
            .property(new GlobalStringProperty(""))
            .key("storageGitRemote")
            .valueClass(String.class)
            .requiresRestart(true)
            .documentationLink(DocumentationLink.SYNC)
            .build());
    final ObjectProperty<SyncMode> syncMode = map(Mapping.builder()
            .property(new GlobalObjectProperty<>(SyncMode.INSTANT))
            .key("syncMode")
            .valueClass(SyncMode.class)
            .requiresRestart(true)
            .documentationLink(DocumentationLink.SYNC_MODE)
            .build());
    final ObjectProperty<CloseBehaviour> closeBehaviour = map(Mapping.builder()
            .property(new GlobalObjectProperty<>(CloseBehaviour.QUIT))
            .key("closeBehaviour")
            .valueClass(CloseBehaviour.class)
            .build());
    final ObjectProperty<ExternalEditorType> externalEditor =
            mapLocal(new GlobalObjectProperty<>(), "externalEditor", ExternalEditorType.class, false);
    final StringProperty customEditorCommand =
            mapLocal(new GlobalStringProperty(""), "customEditorCommand", String.class, false);
    final BooleanProperty customEditorCommandInTerminal =
            mapLocal(new GlobalBooleanProperty(false), "customEditorCommandInTerminal", Boolean.class, false);
    final BooleanProperty automaticallyCheckForUpdates =
            mapLocal(new GlobalBooleanProperty(true), "automaticallyCheckForUpdates", Boolean.class, false);
    final BooleanProperty encryptAllVaultData =
            mapVaultShared(new GlobalBooleanProperty(false), "encryptAllVaultData", Boolean.class, true);
    final BooleanProperty enableTerminalLogging = map(Mapping.builder()
            .property(new GlobalBooleanProperty(false))
            .key("enableTerminalLogging")
            .valueClass(Boolean.class)
            .documentationLink(DocumentationLink.TERMINAL_LOGGING)
            .build());
    final BooleanProperty enableTerminalStartupBell = map(Mapping.builder()
            .property(new GlobalBooleanProperty(false))
            .key("enableTerminalStartupBell")
            .valueClass(Boolean.class)
            .build());
    final BooleanProperty checkForSecurityUpdates =
            mapLocal(new GlobalBooleanProperty(true), "checkForSecurityUpdates", Boolean.class, false);
    final BooleanProperty disableHttpsTlsCheck =
            mapLocal(new GlobalBooleanProperty(false), "disableHttpsTlsCheck", Boolean.class, false);
    final BooleanProperty condenseConnectionDisplay =
            mapLocal(new GlobalBooleanProperty(false), "condenseConnectionDisplay", Boolean.class, false);
    final BooleanProperty showChildCategoriesInParentCategory =
            mapLocal(new GlobalBooleanProperty(true), "showChildrenConnectionsInParentCategory", Boolean.class, false);
    final Property<HibernateBehaviour> hibernateBehaviour =
            mapLocal(new GlobalObjectProperty<>(), "hibernateBehaviour", HibernateBehaviour.class, false);
    final BooleanProperty openConnectionSearchWindowOnConnectionCreation = mapLocal(
            new GlobalBooleanProperty(true), "openConnectionSearchWindowOnConnectionCreation", Boolean.class, false);
    final ObjectProperty<FilePath> downloadsDirectory =
            mapLocal(new GlobalObjectProperty<>(), "downloadsDirectory", FilePath.class, false);
    final BooleanProperty developerMode =
            mapLocal(new GlobalBooleanProperty(false), "developerMode", Boolean.class, true);
    final BooleanProperty developerDisableUpdateVersionCheck =
            mapLocal(new GlobalBooleanProperty(false), "developerDisableUpdateVersionCheck", Boolean.class, false);
    final BooleanProperty developerForceSshTty =
            mapLocal(new GlobalBooleanProperty(false), "developerForceSshTty", Boolean.class, false);
    final BooleanProperty developerDisableSshTunnelGateways =
            mapLocal(new GlobalBooleanProperty(false), "developerDisableSshTunnelGateways", Boolean.class, false);
    final BooleanProperty developerPrintInitFiles =
            mapLocal(new GlobalBooleanProperty(false), "developerPrintInitFiles", Boolean.class, false);
    final BooleanProperty developerShowSensitiveCommands =
            mapLocal(new GlobalBooleanProperty(false), "developerShowSensitiveCommands", Boolean.class, false);
    final BooleanProperty disableSshPinCaching =
            mapLocal(new GlobalBooleanProperty(false), "disableSshPinCaching", Boolean.class, false);
    final ObjectProperty<SupportedLocale> language =
            mapLocal(new GlobalObjectProperty<>(SupportedLocale.ENGLISH), "language", SupportedLocale.class, false);
    final ObjectProperty<FilePath> sshAgentSocket = map(Mapping.builder()
            .property(new GlobalObjectProperty<>())
            .key("sshAgentSocket")
            .valueClass(FilePath.class)
            .requiresRestart(false)
            .build());

    final ObjectProperty<FilePath> defaultSshAgentSocket = new SimpleObjectProperty<>();

    final BooleanProperty requireDoubleClickForConnections =
            mapLocal(new GlobalBooleanProperty(false), "requireDoubleClickForConnections", Boolean.class, false);
    final BooleanProperty editFilesWithDoubleClick =
            mapLocal(new GlobalBooleanProperty(false), "editFilesWithDoubleClick", Boolean.class, false);
    final BooleanProperty enableFileBrowserTerminalDocking =
            mapLocal(new GlobalBooleanProperty(true), "enableFileBrowserTerminalDocking", Boolean.class, false);
    final BooleanProperty enableConnectionHubTerminalDocking =
            mapLocal(new GlobalBooleanProperty(true), "enableConnectionHubTerminalDocking", Boolean.class, false);
    final BooleanProperty censorMode = mapLocal(new GlobalBooleanProperty(false), "censorMode", Boolean.class, false);
    final BooleanProperty sshVerboseOutput = map(Mapping.builder()
            .property(new GlobalBooleanProperty(false))
            .key("sshVerboseOutput")
            .valueClass(Boolean.class)
            .documentationLink(DocumentationLink.SSH_TROUBLESHOOT)
            .build());
    final StringProperty apiKey =
            mapVaultShared(new GlobalStringProperty(UUID.randomUUID().toString()), "apiKey", String.class, true);
    final BooleanProperty disableApiAuthentication =
            mapLocal(new GlobalBooleanProperty(false), "disableApiAuthentication", Boolean.class, false);
    final BooleanProperty allowExternalApiRequests =
            mapLocal(new GlobalBooleanProperty(false), "allowExternalApiRequests", Boolean.class, false);

    @Getter
    private final StringProperty lockCrypt =
            mapVaultShared(new GlobalStringProperty(), "workspaceLock", String.class, true);

    @Getter
    private final List<AppPrefsCategory> categories = List.of(
            new AboutCategory(),
            new PersonalizationCategory(),
            new VaultCategory(),
            new SyncCategory(),
            new PasswordManagerCategory(),
            new TerminalCategory(),
            new EditorCategory(),
            new RdpCategory(),
            new VncCategory(),
            new SshCategory(),
            new ConnectionHubCategory(),
            new FileBrowserCategory(),
            new IconsCategory(),
            new DisplayCategory(),
            new SystemCategory(),
            new ApiCategory(),
            new McpCategory(),
            new UpdatesCategory(),
            new HttpProxyCategory(),
            new SecurityCategory(),
            new WorkspacesCategory(),
            new DeveloperCategory(),
            new TroubleshootCategory(),
            new LinksCategory());

    private final AppPrefsStorageHandler globalStorageHandler = new AppPrefsStorageHandler(
            AppProperties.get().getDataDir().resolve("settings").resolve("preferences.json"));
    private final Map<Mapping, OptionsBuilder> customEntries = new LinkedHashMap<>();

    @Getter
    private final Property<AppPrefsCategory> selectedCategory = new GlobalObjectProperty<>(categories.getFirst());

    private final PrefsHandler extensionHandler = new PrefsHandlerImpl();
    private AppPrefsStorageHandler vaultStorageHandler;

    private AppPrefs() {}

    public boolean canSaveLocal() {
        return globalStorageHandler.isInitialized();
    }

    public ObservableValue<Integer> backgroundSessionInactivityTimeout() {
        return backgroundSessionInactivityTimeout;
    }

    public ObservableBooleanValue useExternalNetcatForProxies() {
        return useExternalNetcatForProxies;
    }

    public ObservableValue<Boolean> disableHttpsTlsCheck() {
        return disableHttpsTlsCheck;
    }

    public ObservableBooleanValue hideVaultEntryNames() {
        return hideVaultEntryNames;
    }

    public ObservableValue<VaultAuthentication> vaultAuthentication() {
        return vaultAuthentication;
    }

    public ObservableValue<TerminalSplitStrategy> terminalSplitStrategy() {
        return terminalSplitStrategy;
    }

    public ObservableStringValue notesTemplate() {
        return notesTemplate;
    }

    public BooleanProperty disableHardwareAcceleration() {
        return disableHardwareAcceleration;
    }

    public ObservableBooleanValue enableTerminalStartupBell() {
        return enableTerminalStartupBell;
    }

    public ObservableBooleanValue preferTerminalTabs() {
        return preferTerminalTabs;
    }

    public ObservableValue<List<SystemIconSource>> getIconSources() {
        return iconSources;
    }

    public ObservableValue<TerminalPrompt> terminalPrompt() {
        return terminalPrompt;
    }

    public ObservableValue<UUID> terminalProxy() {
        return terminalProxy;
    }

    public ObservableValue<Boolean> terminalAlwaysPauseOnExit() {
        return terminalAlwaysPauseOnExit;
    }

    public ObservableValue<FilePath> sshAgentSocket() {
        return sshAgentSocket;
    }

    public ObservableValue<FilePath> defaultSshAgentSocket() {
        return defaultSshAgentSocket;
    }

    public ObservableBooleanValue preferMonochromeIcons() {
        return preferMonochromeIcons;
    }

    public ObservableBooleanValue editFilesWithDoubleClick() {
        return editFilesWithDoubleClick;
    }

    public ObservableBooleanValue sshVerboseOutput() {
        return sshVerboseOutput;
    }

    public ObservableValue<SyncMode> syncMode() {
        return syncMode;
    }

    public ObservableBooleanValue censorMode() {
        return censorMode;
    }

    public ObservableBooleanValue requireDoubleClickForConnections() {
        return requireDoubleClickForConnections;
    }

    public ObservableBooleanValue enableFileBrowserTerminalDocking() {
        return enableFileBrowserTerminalDocking;
    }

    public ObservableBooleanValue enableConnectionHubTerminalDocking() {
        return enableConnectionHubTerminalDocking;
    }

    public ObservableBooleanValue disableSshPinCaching() {
        return disableSshPinCaching;
    }

    public ObservableBooleanValue focusWindowOnNotifications() {
        return focusWindowOnNotifications;
    }

    public ObservableValue<AppTheme.Theme> theme() {
        return theme;
    }

    public ObservableBooleanValue developerPrintInitFiles() {
        return developerPrintInitFiles;
    }

    public ObservableBooleanValue developerShowSensitiveCommands() {
        return developerShowSensitiveCommands;
    }

    public ObservableBooleanValue checkForSecurityUpdates() {
        return checkForSecurityUpdates;
    }

    public ObservableBooleanValue enableTerminalLogging() {
        return enableTerminalLogging;
    }

    public ObservableStringValue apiKey() {
        return apiKey;
    }

    public ObservableValue<HttpProxy> httpProxy() {
        return httpProxy;
    }

    public ObservableBooleanValue disableApiAuthentication() {
        return disableApiAuthentication;
    }

    public ObservableBooleanValue allowExternalApiRequests() {
        return allowExternalApiRequests;
    }

    public ObservableBooleanValue enableHttpApi() {
        return enableHttpApi;
    }

    public ObservableBooleanValue enableMcpServer() {
        return enableMcpServer;
    }

    public ObservableValue<String> mcpAdditionalContext() {
        return mcpAdditionalContext;
    }

    public ObservableBooleanValue pinLocalMachineOnStartup() {
        return pinLocalMachineOnStartup;
    }

    public ObservableValue<PasswordManager> passwordManager() {
        return passwordManager;
    }

    public ObservableValue<TerminalMultiplexer> terminalMultiplexer() {
        return terminalMultiplexer;
    }

    @SuppressWarnings("unused")
    public ObservableValue<ShellScript> terminalInitScript() {
        return terminalInitScript;
    }

    public ObservableValue<SupportedLocale> language() {
        return language;
    }

    public ObservableBooleanValue dontAutomaticallyStartVmSshServer() {
        return dontAutomaticallyStartVmSshServer;
    }

    public ObservableBooleanValue dontAcceptNewHostKeys() {
        return dontAcceptNewHostKeys;
    }

    public ObservableBooleanValue performanceMode() {
        return performanceMode;
    }

    public ObservableBooleanValue limitedTouchscreenMode() {
        return limitedTouchscreenMode;
    }

    public ObservableValue<Boolean> useSystemFont() {
        return useSystemFont;
    }

    public ReadOnlyProperty<Integer> uiScale() {
        return uiScale;
    }

    public ReadOnlyBooleanProperty clearTerminalOnInit() {
        return clearTerminalOnInit;
    }

    public ObservableBooleanValue disableCertutilUse() {
        return disableCertutilUse;
    }

    public ObservableValue<ShellDialect> localShellDialect() {
        return localShellDialect;
    }

    public ObservableBooleanValue disableTerminalRemotePasswordPreparation() {
        return disableTerminalRemotePasswordPreparation;
    }

    public ObservableValue<HibernateBehaviour> hibernateBehaviour() {
        return hibernateBehaviour;
    }

    public ObservableValue<Boolean> alwaysConfirmElevation() {
        return alwaysConfirmElevation;
    }

    public ObservableBooleanValue dontCachePasswords() {
        return dontCachePasswords;
    }

    public ObservableBooleanValue enableGitStorage() {
        return enableGitStorage;
    }

    public ObservableStringValue storageGitRemote() {
        return storageGitRemote;
    }

    public ObservableBooleanValue encryptAllVaultData() {
        return encryptAllVaultData;
    }

    public ObservableBooleanValue condenseConnectionDisplay() {
        return condenseConnectionDisplay;
    }

    public ObservableBooleanValue showChildCategoriesInParentCategory() {
        return showChildCategoriesInParentCategory;
    }

    public ObservableBooleanValue openConnectionSearchWindowOnConnectionCreation() {
        return openConnectionSearchWindowOnConnectionCreation;
    }

    public ReadOnlyProperty<CloseBehaviour> closeBehaviour() {
        return closeBehaviour;
    }

    public ReadOnlyProperty<ExternalEditorType> externalEditor() {
        return externalEditor;
    }

    public ObservableValue<String> customEditorCommand() {
        return customEditorCommand;
    }

    public ObservableBooleanValue customEditorCommandInTerminal() {
        return customEditorCommandInTerminal;
    }

    public ReadOnlyProperty<StartupBehaviour> startupBehaviour() {
        return startupBehaviour;
    }

    public ReadOnlyBooleanProperty automaticallyUpdate() {
        return automaticallyCheckForUpdates;
    }

    public ObservableValue<ExternalTerminalType> terminalType() {
        return terminalType;
    }

    public ObservableValue<ExternalRdpClient> rdpClientType() {
        return rdpClientType;
    }

    public ObservableValue<String> customTerminalCommand() {
        return customTerminalCommand;
    }

    public ObservableValue<FilePath> downloadsDirectory() {
        return downloadsDirectory;
    }

    public ObservableValue<Boolean> developerMode() {
        return AppProperties.get().isDeveloperMode() ? new ReadOnlyBooleanWrapper(true) : developerMode;
    }

    public ObservableDoubleValue windowOpacity() {
        return windowOpacity;
    }

    public ObservableBooleanValue saveWindowLocation() {
        return saveWindowLocation;
    }

    public ObservableBooleanValue developerDisableUpdateVersionCheck() {
        return developerDisableUpdateVersionCheck;
    }

    public ObservableBooleanValue developerForceSshTty() {
        return developerForceSshTty;
    }

    public ObservableBooleanValue developerDisableSshTunnelGateways() {
        return developerDisableSshTunnelGateways;
    }

    @SuppressWarnings("unchecked")
    private <T> T map(Mapping m) {
        mapping.add(m);
        m.property.addListener((observable, oldValue, newValue) -> {
            var running = AppOperationMode.get() == AppOperationMode.GUI;
            if (running && m.requiresRestart) {
                AppPrefs.get().requiresRestart.set(true);
            }
        });
        return (T) m.getProperty();
    }

    private <T> T mapLocal(Property<?> o, String name, Class<?> clazz, boolean requiresRestart) {
        return map(new Mapping(name, o, clazz, false, requiresRestart, true, null));
    }

    private <T> T mapVaultShared(Property<?> o, String name, Class<?> clazz, boolean requiresRestart) {
        return map(new Mapping(name, o, clazz, true, requiresRestart, true, null));
    }

    public <T> void setFromExternal(ObservableValue<T> prop, T newValue) {
        var writable = (Property<T>) prop;

        // Prior to GUI init, we can set whatever we like
        if (AppLayoutModel.get() == null) {
            writable.setValue(newValue);
            return;
        }

        PlatformThread.runLaterIfNeededBlocking(() -> {
            writable.setValue(newValue);
        });

        if (mapping.stream().anyMatch(m -> m.property == prop)) {
            save();
        }
    }

    private void fixLocalValues() {
        uiScale.setValue(AppDisplayScale.clampValue(uiScale.getValue()));

        if (AppDistributionType.get() == AppDistributionType.WEBTOP) {
            performanceMode.setValue(true);
        } else if (System.getProperty("os.name").toLowerCase().contains("server")) {
            performanceMode.setValue(true);
        }

        if (!AppProperties.get().isDevelopmentEnvironment()) {
            developerForceSshTty.setValue(false);
            developerDisableSshTunnelGateways.setValue(false);
        }

        if (OsType.ofLocal() == OsType.MACOS
                && AppProperties.get()
                        .getCanonicalVersion()
                        .map(appVersion -> appVersion.getMajor() == 18)
                        .orElse(false)) {
            useSystemFont.set(false);
        }

        if (useLocalFallbackShell.get()) {
            localShellDialect.setValue(
                    ProcessControlProvider.get().getAvailableLocalDialects().get(1));
            useLocalFallbackShell.set(false);
        }

        if (localShellDialect.getValue() == null
                || !ProcessControlProvider.get().getAvailableLocalDialects().contains(localShellDialect.getValue())) {
            localShellDialect.setValue(
                    ProcessControlProvider.get().getAvailableLocalDialects().getFirst());
        }

        if (sshVerboseOutput.get()) {
            sshVerboseOutput.set(false);
        }

        if (enableMcpServer.get()) {
            enableHttpApi.set(true);
        }

        PrefsProvider.getAll().forEach(prov -> prov.fixLocalValues());
    }

    public void initDefaultValues() {
        externalEditor.setValue(ExternalEditorType.determineDefault(externalEditor.get()));
        terminalType.set(ExternalTerminalType.determineDefault(terminalType.get()));
        rdpClientType.setValue(ExternalRdpClient.determineDefault(rdpClientType.get()));
        spiceClient.setValue(ExternalSpiceClient.determineDefault(spiceClient.getValue()));
        passwordManager.setValue(PasswordManager.determineDefault(passwordManager.getValue()));
        terminalMultiplexer.setValue(TerminalMultiplexer.determineDefault(terminalMultiplexer.getValue()));

        PrefsProvider.getAll().forEach(prov -> prov.initDefaultValues());
    }

    public OptionsBuilder getCustomOptions(String id) {
        return customEntries.entrySet().stream()
                .filter(e -> e.getKey().getKey().equals(id))
                .findFirst()
                .map(Map.Entry::getValue)
                .orElseThrow();
    }

    private void loadLocal() {
        for (Mapping value : mapping) {
            if (value.isVaultSpecific()) {
                continue;
            }

            loadValue(globalStorageHandler, value);
        }
    }

    private void loadSharedRemote() throws Exception {
        for (Mapping value : mapping) {
            if (!value.isVaultSpecific()) {
                continue;
            }

            loadValue(vaultStorageHandler, value);
        }

        if (OsType.ofLocal() != OsType.WINDOWS) {
            var changeDate = LocalDate.of(2026, 3, 25)
                    .atStartOfDay(ZoneId.systemDefault())
                    .toInstant();
            var removePasswordManagerAgent =
                    AppProperties.get().getFirstStartupDate().compareTo(changeDate) > 0;

            // On Linux and macOS, we prefer the shell variable compared to any global env variable
            // as the one is set by default and might not be the right one
            // This happens for example with homebrew ssh
            var shellVariable = LocalShell.getShell().view().getEnvironmentVariable("SSH_AUTH_SOCK");
            if (shellVariable.isPresent()
                    && removePasswordManagerAgent
                    && PasswordManager.isPasswordManagerSshAgent(shellVariable.get())) {
                shellVariable = Optional.empty();
            }

            var envVariable = System.getenv("SSH_AUTH_SOCK");
            if (envVariable != null
                    && removePasswordManagerAgent
                    && PasswordManager.isPasswordManagerSshAgent(envVariable)) {
                envVariable = null;
            }

            var socketEnvVariable = shellVariable.isEmpty() ? envVariable : shellVariable.get();
            defaultSshAgentSocket.setValue(FilePath.parse(socketEnvVariable));
        }

        try {
            var file = AppProperties.get().getDataDir().resolve("storage").resolve("notes.md");
            if (Files.exists(file)) {
                notesTemplate.set(Files.readString(file));
            }
        } catch (Exception e) {
            ErrorEventFactory.fromThrowable(e).handle();
        }
    }

    @SuppressWarnings("unchecked")
    private <T> void loadValue(AppPrefsStorageHandler handler, Mapping value) {
        T def = (T) value.getProperty().getValue();
        Property<T> property = (Property<T>) value.getProperty();
        var val = handler.loadObject(value.getKey(), value.getValueType(), def, value.isLog());
        property.setValue(val);
    }

    public synchronized void save() {
        for (Mapping m : mapping) {
            AppPrefsStorageHandler handler = m.isVaultSpecific() ? vaultStorageHandler : globalStorageHandler;
            // It might be possible that we save while the vault handler is not initialized yet / has no file or
            // directory
            if (!handler.isInitialized()) {
                continue;
            }
            handler.updateObject(m.getKey(), m.getProperty().getValue(), m.getValueType());
        }
        if (vaultStorageHandler.isInitialized()) {
            vaultStorageHandler.save();
        }
        if (globalStorageHandler.isInitialized()) {
            globalStorageHandler.save();
        }

        if (notesTemplate.get() != null) {
            try {
                Files.writeString(
                        AppProperties.get().getDataDir().resolve("storage", "notes.md"), notesTemplate.getValue());
            } catch (Exception e) {
                ErrorEventFactory.fromThrowable(e).handle();
            }
        }
    }

    public void selectCategory(String id) {
        var found = categories.stream()
                .filter(appPrefsCategory -> appPrefsCategory.getId().equals(id))
                .findFirst();
        found.ifPresent(appPrefsCategory -> {
            PlatformThread.runLaterIfNeeded(() -> {
                AppLayoutModel.get().selectSettings();

                GlobalTimer.delay(
                        () -> {
                            Platform.runLater(() -> {
                                // Reset scroll in case the target category is already somewhat in focus
                                selectedCategory.setValue(null);
                                selectedCategory.setValue(appPrefsCategory);
                            });
                        },
                        Duration.ofMillis(100));
            });
        });
    }

    public Mapping getMapping(Object property) {
        return mapping.stream().filter(m -> m.property == property).findFirst().orElseThrow();
    }

    @Value
    @Builder
    @AllArgsConstructor
    public static class Mapping {

        String key;
        Property<?> property;
        JavaType valueType;
        boolean vaultSpecific;
        boolean requiresRestart;
        String licenseFeatureId;
        boolean log;
        DocumentationLink documentationLink;

        public Mapping(
                String key,
                Property<?> property,
                Class<?> valueType,
                boolean vaultSpecific,
                boolean requiresRestart,
                boolean log,
                DocumentationLink documentationLink) {
            this.key = key;
            this.property = property;
            this.valueType = SimpleType.constructUnsafe(valueType);
            this.vaultSpecific = vaultSpecific;
            this.requiresRestart = requiresRestart;
            this.log = log;
            this.documentationLink = documentationLink;
            this.licenseFeatureId = null;
        }

        public Mapping(
                String key,
                Property<?> property,
                JavaType valueType,
                boolean vaultSpecific,
                boolean requiresRestart,
                boolean log,
                DocumentationLink documentationLink) {
            this.key = key;
            this.property = property;
            this.valueType = valueType;
            this.vaultSpecific = vaultSpecific;
            this.requiresRestart = requiresRestart;
            this.log = log;
            this.documentationLink = documentationLink;
            this.licenseFeatureId = null;
        }

        public static class MappingBuilder {

            MappingBuilder valueClass(Class<?> clazz) {
                this.valueType(TypeFactory.defaultInstance().constructType(clazz));
                return this;
            }
        }
    }

    @Getter
    private class PrefsHandlerImpl implements PrefsHandler {

        @Override
        public <T> void addSetting(
                String id,
                JavaType t,
                Property<T> property,
                OptionsBuilder builder,
                boolean requiresRestart,
                boolean log) {
            var m = new Mapping(id, property, t, false, requiresRestart, log, null);
            customEntries.put(m, builder);
            map(m);
        }
    }
}
