#!/usr/bin/env python3
#
# Copyright (c) 2024 Stormux
# Copyright (c) 2010-2012 The Orca Team
# Copyright (c) 2012 Igalia, S.L.
# Copyright (c) 2005-2010 Sun Microsystems Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu

"""Settings manager module. This will load/save user settings from a 
defined settings backend."""

from __future__ import annotations

__id__        = "$Id$"
__version__   = "$Revision$"
__date__      = "$Date$"
__copyright__ = "Copyright (c) 2010 Consorcio Fernando de los Rios."
__license__   = "LGPL"

import copy
import importlib
import os
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union

from gi.repository import Gio, GLib
from tomlkit import parse

from . import debug
from . import cthulhu_i18n
from . import settings
from . import pronunciation_dict
from .acss import ACSS
from .ax_object import AXObject
from .keybindings import KeyBinding

if TYPE_CHECKING:
    from .cthulhu import Cthulhu
    from .script import Script
    from .input_event import InputEventHandler

# Removed global cthulhuApp.scriptManager declaration.
# Note: Do not import cthulhu module here to avoid circular import

class SettingsManager(object):
    """Settings backend manager. This class manages cthulhu user's settings
    using different backends"""

    def __init__(self, app: Cthulhu, backend: str = 'toml') -> None: # Modified signature

        debug.printMessage(debug.LEVEL_INFO, 'SETTINGS MANAGER: Initializing', True)
        self.app: Cthulhu = app # Store app instance
        
        # Move _proxy initialization here
        try:
            self._proxy: Optional[Gio.DBusProxy] = Gio.DBusProxy.new_for_bus_sync(
                Gio.BusType.SESSION,
                Gio.DBusProxyFlags.NONE,
                None,
                'org.a11y.Bus',
                '/org/a11y/bus',
                'org.freedesktop.DBus.Properties',
                None)
        except Exception:
            self._proxy = None

        self.backendModule: Optional[Any] = None
        self._backend: Optional[Any] = None
        self.profile: Optional[str] = None
        self.backendName: str = backend
        self._prefsDir: Optional[str] = None

        # Dictionaries for store the default values
        # The keys and values are defined at cthulhu.settings
        #
        self.defaultGeneral: Dict[str, Any] = {}
        self.defaultPronunciations: Dict[str, Any] = {}
        self.defaultKeybindings: Dict[str, Any] = {}

        # Dictionaries that store the key:value pairs which values are
        # different from the current profile and the default ones
        #
        self.profileGeneral: Dict[str, Any] = {}
        self.profilePronunciations: Dict[str, Any] = {}
        self.profileKeybindings: Dict[str, Any] = {}

        # Dictionaries that store the current settings.
        # They are result to overwrite the default values with
        # the ones from the current active profile
        self.general: Dict[str, Any] = {}
        self.pronunciations: Dict[str, Any] = {}
        self.keybindings: Dict[str, Any] = {}

        self._activeApp: str = ""
        self._appGeneral: Dict[str, Any] = {}
        self._appPronunciations: Dict[str, Any] = {}
        self._appKeybindings: Dict[str, Any] = {}
        self._lastRoleSoundPresentation: Optional[Any] = None

        if not self._loadBackend():
            raise Exception('SettingsManager._loadBackend failed.')

        self.customizedSettings: Dict[str, Any] = {}
        self._customizationCompleted: bool = False

        # For handling the currently-"classic" application settings
        self.settingsPackages: List[str] = ["app-settings"]

        debug.printMessage(debug.LEVEL_INFO, 'SETTINGS MANAGER: Initialized', True)

    def activate(self, prefsDir: Optional[str] = None, customSettings: Dict[str, Any] = {}) -> None:
        debug.printMessage(debug.LEVEL_INFO, 'SETTINGS MANAGER: Activating', True)

        self.customizedSettings.update(customSettings)
        self._prefsDir = prefsDir \
            or os.path.join(GLib.get_user_data_dir(), "cthulhu")

        # Load the backend and the default values
        if self.backendModule:
            self._backend = self.backendModule.Backend(self._prefsDir)
        self._setDefaultGeneral()
        self._setDefaultPronunciations()
        self._setDefaultKeybindings()
        self.general = self.defaultGeneral.copy()
        if not self.isFirstStart() and self._backend:
            self.general.update(self._backend.getGeneral())
        self.pronunciations = self.defaultPronunciations.copy()
        self.keybindings = self.defaultKeybindings.copy()

        # If this is the first time we launch Cthulhu, there is no user settings
        # yet, so we need to create the user config directories and store the
        # initial default settings
        #
        self._createDefaults()

        debug.printMessage(debug.LEVEL_INFO, 'SETTINGS MANAGER: Activated', True)

        # Set the active profile and load its stored settings
        tokens = ["SETTINGS MANAGER: Current profile is", self.profile]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)

        if self.profile is None:
            self.profile = self.general.get('startingProfile')[1] # type: ignore
            tokens = ["SETTINGS MANAGER: Current profile is now", self.profile]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)

        if self.profile:
            self.setProfile(self.profile)

    def _loadBackend(self) -> bool:
        """Load specific backend for manage user settings"""

        try:
            backend = f'.backends.{self.backendName}_backend'
            self.backendModule = importlib.import_module(backend, 'cthulhu')
            return True
        except Exception:
            return False

    def _createDefaults(self) -> None:
        """Let the active backend to create the initial structure
        for storing the settings and save the default ones from
        cthulhu.settings"""
        def _createDir(dirName: str) -> None:
            if not os.path.isdir(dirName):
                os.makedirs(dirName)

        if not self._prefsDir:
            return

        # Set up the user's preferences directory
        # ($XDG_DATA_HOME/cthulhu by default).
        #
        cthulhuDir = self._prefsDir
        _createDir(cthulhuDir)

        # Set up $XDG_DATA_HOME/cthulhu/cthulhu-scripts as a Python package
        #
        cthulhuScriptDir = os.path.join(cthulhuDir, "cthulhu-scripts")
        _createDir(cthulhuScriptDir)
        initFile = os.path.join(cthulhuScriptDir, "__init__.py")
        if not os.path.exists(initFile):
            os.close(os.open(initFile, os.O_CREAT, 0o700))

        cthulhuSettingsDir = os.path.join(cthulhuDir, "app-settings")
        _createDir(cthulhuSettingsDir)

        cthulhuSoundsDir = os.path.join(cthulhuDir, "sounds")
        _createDir(cthulhuSoundsDir)

        # Set up $XDG_DATA_HOME/cthulhu/cthulhu-customizations.py empty file and
        # define cthulhuDir as a Python package.
        initFile = os.path.join(cthulhuDir, "__init__.py")
        if not os.path.exists(initFile):
            os.close(os.open(initFile, os.O_CREAT, 0o700))

        userCustomFile = os.path.join(cthulhuDir, "cthulhu-customizations.py")
        if not os.path.exists(userCustomFile):
            os.close(os.open(userCustomFile, os.O_CREAT, 0o700))

        sleepConfigFile = os.path.join(cthulhuDir, "sleep.toml")
        if not os.path.exists(sleepConfigFile):
            sleepTemplate = (
                "# Cthulhu auto-sleep apps\n"
                "#\n"
                "# List current app names with:\n"
                "#   cthulhu --list-apps\n"
                "# Use the middle app-name column from that output.\n"
                "#\n"
                "# Add app names to auto-enable sleep mode:\n"
                "# apps = [\"qemu\"]\n"
                "#\n"
                "# Or use a section:\n"
                "# [sleep]\n"
                "# apps = [\"qemu\"]\n"
            )
            with open(sleepConfigFile, "w", encoding="utf-8") as configFile:
                configFile.write(sleepTemplate)

        if self.isFirstStart() and self._backend:
            self._backend.saveDefaultSettings(self.defaultGeneral,
                                              self.defaultPronunciations,
                                              self.defaultKeybindings)

    def _setDefaultPronunciations(self) -> None:
        """Get the pronunciations by default from cthulhu.settings"""
        self.defaultPronunciations = {}

    def _setDefaultKeybindings(self) -> None:
        """Get the keybindings by default from cthulhu.settings"""
        self.defaultKeybindings = {}

    def _setDefaultGeneral(self) -> None:
        """Get the general settings by default from cthulhu.settings"""
        self._getCustomizedSettings()
        self.defaultGeneral = {}
        for key in settings.userCustomizableSettings:
            value = self.customizedSettings.get(key)
            if value is None:
                try:
                    value = getattr(settings, key)
                except Exception:
                    pass
            try:
                value = copy.deepcopy(value)
            except Exception:
                pass
            self.defaultGeneral[key] = value
        default_active_plugins = copy.deepcopy(self.defaultGeneral.get("activePlugins"))
        overrides = self._load_default_general_overrides()
        if overrides:
            for key, value in overrides.items():
                if key not in self.defaultGeneral:
                    continue
                if key == "activePlugins":
                    continue
                self.defaultGeneral[key] = value
        if default_active_plugins is not None:
            self.defaultGeneral["activePlugins"] = default_active_plugins

    def _load_default_general_overrides(self) -> Dict[str, Any]:
        if not self._prefsDir:
            return {}

        settings_path = os.path.join(self._prefsDir, "user-settings.toml")
        if not os.path.exists(settings_path):
            return {}

        try:
            with open(settings_path, "r", encoding="utf-8") as settings_file:
                prefs = parse(settings_file.read())
        except Exception as error:
            msg = f"SETTINGS MANAGER: Unable to read default settings from {settings_path}: {error}"
            debug.printMessage(debug.LEVEL_WARNING, msg, True)
            return {}

        general = prefs.get("general")
        if not isinstance(general, dict):
            return {}

        if self._backend and hasattr(self._backend, "_migrateSettings"):
            try:
                general = self._backend._migrateSettings(dict(general))
            except Exception as error:
                msg = f"SETTINGS MANAGER: Unable to migrate default settings: {error}"
                debug.printMessage(debug.LEVEL_WARNING, msg, True)
                general = dict(general)
        else:
            general = dict(general)

        voices = general.get("voices")
        if isinstance(voices, dict):
            converted_voices = {}
            for voice_type, voice_def in voices.items():
                try:
                    converted_voices[voice_type] = ACSS(voice_def)
                except Exception:
                    converted_voices[voice_type] = voice_def
            general["voices"] = converted_voices

        echo_voice = general.get("echoVoice")
        if isinstance(echo_voice, dict):
            try:
                general["echoVoice"] = ACSS(echo_voice)
            except Exception:
                general["echoVoice"] = echo_voice

        return general

    def getDefaultSetting(self, settingName: str) -> Any:
        if settingName in self.defaultGeneral:
            return self.defaultGeneral.get(settingName)
        return getattr(settings, settingName, None)

    def _getCustomizedSettings(self) -> Dict[str, Any]:
        if self._customizationCompleted:
            return self.customizedSettings

        originalSettings = {}
        for key, value in settings.__dict__.items():
            originalSettings[key] = value

        self._customizationCompleted = self._loadUserCustomizations()

        for key, value in originalSettings.items():
            customValue = settings.__dict__.get(key)
            if value != customValue:
                self.customizedSettings[key] = customValue
        
        return self.customizedSettings

    def _loadUserCustomizations(self) -> bool:
        """Attempt to load the user's cthulhu-customizations. Returns a boolean
        indicating our success at doing so, where success is measured by the
        likelihood that the results won't be different if we keep trying."""

        success = False
        if not self._prefsDir:
            return False

        pathList = [self._prefsDir]
        tokens = ["SETTINGS MANAGER: Attempt to load cthulhu-customizations"]
        module_path = pathList[0] + "/cthulhu-customizations.py"

        try:
            spec = importlib.util.spec_from_file_location("cthulhu-customizations", module_path)
            if spec is not None and spec.loader:
                module = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(module)
                tokens.extend(["from", module_path, "succeeded."])
                success = True
            else:
                tokens.extend(["from", module_path, "failed. Spec not found."])
        except FileNotFoundError:
            tokens.extend(["from", module_path, "failed. File not found."])
        except Exception as error:
            # Treat this failure as a "success" so that we don't stomp on the existing file.
            success = True
            tokens.extend(["failed due to:", str(error), ". Not loading customizations."])

        debug.printTokens(debug.LEVEL_ALL, tokens, True)
        return success

    def getPrefsDir(self) -> Optional[str]:
        return self._prefsDir

    def setSetting(self, settingName: str, settingValue: Any) -> None:
        self._setSettingsRuntime({settingName:settingValue})

    def getSetting(self, settingName: str) -> Any:
        return getattr(settings, settingName, None)

    def _getListSetting(self, settingName: str) -> List[str]:
        value = self.getSetting(settingName)
        if isinstance(value, (list, tuple)):
            return [item for item in value if isinstance(item, str)]
        return []

    def _ensurePluginPersistenceSettings(self, general: Dict[str, Any]) -> None:
        if 'activePlugins' not in general:
            general['activePlugins'] = self._getListSetting('activePlugins')
        if 'pluginSources' not in general:
            general['pluginSources'] = self._getListSetting('pluginSources')

    def getVoiceLocale(self, voice: str = 'default') -> str:
        voices = self.getSetting('voices')
        v = ACSS(voices.get(voice, {}))
        lang = v.getLocale()
        dialect = v.getDialect()
        if dialect and len(str(dialect)) == 2:
            lang = f"{lang}_{dialect.upper()}"
        return lang

    def getSpeechServerFactories(self) -> List[Any]:
        """Imports all known SpeechServer factory modules."""

        factories = []
        for moduleName in self.getSetting('speechFactoryModules'):
            try:
                module = importlib.import_module(f'cthulhu.{moduleName}')
                factories.append(module)
                tokens = ["SETTINGS MANAGER: Valid speech server factory:", moduleName]
                debug.printTokens(debug.LEVEL_INFO, tokens, True)
            except Exception:
                tokens = ["SETTINGS MANAGER: Invalid speech server factory:", moduleName]
                debug.printTokens(debug.LEVEL_INFO, tokens, True)

        return factories

    def _loadProfileSettings(self, profile: Optional[str] = None) -> None:
        """Get from the active backend all the settings for the current
        profile and store them in the object's attributes.
        A profile can be passed as a parameter. This could be useful for
        change from one profile to another."""

        tokens = ["SETTINGS MANAGER: Loading settings for", profile, "profile"]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)

        if profile is None:
            profile = self.profile
        
        if profile:
            self.profileGeneral = self.getGeneralSettings(profile) or {}
            self.profilePronunciations = self.getPronunciations(profile) or {}
            self.profileKeybindings = self.getKeybindings(profile) or {}

        tokens = ["SETTINGS MANAGER: Settings for", profile, "profile loaded"]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)

    def _mergeSettings(self) -> None:
        """Update the changed values on the profile settings
        over the current and active settings"""

        msg = 'SETTINGS MANAGER: Merging settings.'
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        self.profileGeneral.update(self._appGeneral)
        self.profilePronunciations.update(self._appPronunciations)
        self.profileKeybindings.update(self._appKeybindings)

        self.general.update(self.profileGeneral)
        self.pronunciations.update(self.profilePronunciations)
        self.keybindings.update(self.profileKeybindings)

        msg = 'SETTINGS MANAGER: Settings merged.'
        debug.printMessage(debug.LEVEL_INFO, msg, True)

    def _enableAccessibility(self) -> bool:
        """Enables the desktop accessibility bus flag when available.

        Returns True if an action was taken (i.e., accessibility was not
        set prior to this call).
        """

        alreadyEnabled = self.isAccessibilityEnabled()
        if not alreadyEnabled:
            self.setAccessibility(True)

        return not alreadyEnabled

    def isAccessibilityEnabled(self) -> bool:
        msg = 'SETTINGS MANAGER: Checking if accessibility is enabled.'
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        msg = 'SETTINGS MANAGER: Accessibility enabled: '
        rv = False
        if not self._proxy:
            rv = False
            msg += 'Error (no proxy)'
        else:
            try:
                rv = self._proxy.Get('(ss)', 'org.a11y.Status', 'IsEnabled')
                msg += str(rv)
            except Exception:
                rv = False
                msg += 'Error calling DBus'

        debug.printMessage(debug.LEVEL_INFO, msg, True)
        return rv

    def setAccessibility(self, enable: bool) -> Union[bool, None]:
        msg = f'SETTINGS MANAGER: Attempting to set accessibility to {enable}.'
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        if not self._proxy:
            msg = 'SETTINGS MANAGER: Error (no proxy)'
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return False

        try:
            vEnable = GLib.Variant('b', enable)
            self._proxy.Set('(ssv)', 'org.a11y.Status', 'IsEnabled', vEnable)
        except Exception:
            pass

        msg = f'SETTINGS MANAGER: Finished setting accessibility to {enable}.'
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        return None

    def isScreenReaderServiceEnabled(self) -> bool:
        """Returns True if the screen reader service is enabled. Note that
        this does not necessarily mean that Cthulhu (or any other screen reader)
        is running at the moment."""

        msg = 'SETTINGS MANAGER: Is screen reader service enabled? '

        rv = False
        if not self._proxy:
            rv = False
            msg += 'Error (no proxy)'
        else:
            try:
                rv = self._proxy.Get('(ss)', 'org.a11y.Status', 'ScreenReaderEnabled')
                msg += str(rv)
            except Exception:
                rv = False

        debug.printMessage(debug.LEVEL_INFO, msg, True)
        return rv

    def setStartingProfile(self, profile: Optional[tuple] = None) -> None:
        if profile is None:
            profile = settings.profile
        if self._backend:
            self._backend._setProfileKey('startingProfile', profile)

    def getProfile(self) -> Optional[str]:
        return self.profile

    def setProfile(self, profile: str = 'default', updateLocale: bool = False) -> None:
        """Set a specific profile as the active one.
        Also the settings from that profile will be loading
        and updated the current settings with them."""

        tokens = ["SETTINGS MANAGER: Setting profile to:", profile]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)

        oldVoiceLocale = self.getVoiceLocale('default')
        self.profile = profile
        self._loadProfileSettings(profile)
        self._mergeSettings()
        self._setSettingsRuntime(self.general)

        if not updateLocale:
            return

        newVoiceLocale = self.getVoiceLocale('default')
        if oldVoiceLocale != newVoiceLocale:
            cthulhu_i18n.setLocaleForNames(newVoiceLocale)
            cthulhu_i18n.setLocaleForMessages(newVoiceLocale)
            cthulhu_i18n.setLocaleForGUI(newVoiceLocale)

        tokens = ["SETTINGS MANAGER: Profile set to:", profile]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)

    def removeProfile(self, profile: str) -> None:
        if self._backend:
            self._backend.removeProfile(profile)

    def _setSettingsRuntime(self, settingsDict: Dict[str, Any]) -> None:
        msg = 'SETTINGS MANAGER: Setting runtime settings.'
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        for key, value in settingsDict.items():
            setattr(settings, str(key), value)
        self._getCustomizedSettings()
        for key, value in self.customizedSettings.items():
            setattr(settings, str(key), value)

        msg = 'SETTINGS MANAGER: Runtime settings set.'
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        self._logRoleSoundPresentationChange()

    def _logRoleSoundPresentationChange(self) -> None:
        current = getattr(settings, "roleSoundPresentation", None)
        if current == self._lastRoleSoundPresentation:
            return

        msg = (
            "SETTINGS MANAGER: roleSoundPresentation changed to "
            f"{current} profile={self.profile} app={self._activeApp}"
        )
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        self._lastRoleSoundPresentation = current

    def _setPronunciationsRuntime(self, pronunciationsDict: Dict[str, Any]) -> None:
        pronunciation_dict.pronunciation_dict = {}
        for key, value in pronunciationsDict.values():
            if key and value:
                pronunciation_dict.setPronunciation(key, value)

    def getGeneralSettings(self, profile: str = 'default') -> Dict[str, Any]:
        """Return the current general settings.
        Those settings comes from updating the default settings
        with the profiles' ones"""
        general = self.defaultGeneral.copy()
        if self._backend:
            general.update(self._backend.getGeneral(profile))
        return general

    def getPronunciations(self, profile: str = 'default') -> Dict[str, Any]:
        """Return the current pronunciations settings.
        Those settings comes from updating the default settings
        with the profiles' ones"""
        if self._backend:
            return self._backend.getPronunciations(profile)
        return {}

    def getKeybindings(self, profile: str = 'default') -> Dict[str, Any]:
        """Return the current keybindings settings.
        Those settings comes from updating the default settings
        with the profiles' ones"""
        if self._backend:
            return self._backend.getKeybindings(profile)
        return {}

    def _setProfileGeneral(self, general: Dict[str, Any]) -> None:
        """Set the changed general settings from the defaults' ones
        as the profile's."""

        msg = 'SETTINGS MANAGER: Setting general settings for profile'
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        self.profileGeneral = {}
        alwaysPersistKeys = {
            'useCustomEchoVoice',
            'useCustomEchoSpeechServer',
            'useCustomEchoForKey',
            'useCustomEchoForCharacter',
            'useCustomEchoForWord',
            'useCustomEchoForSentence',
        }

        for key, value in general.items():
            if key in ['startingProfile', 'activeProfile']:
                continue
            elif key == 'profile':
                self.profileGeneral[key] = value
            elif key in ['activePlugins', 'pluginSources']:
                self.profileGeneral[key] = copy.deepcopy(value)
            elif key in alwaysPersistKeys:
                self.profileGeneral[key] = copy.deepcopy(value)
            elif value != self.defaultGeneral.get(key):
                self.profileGeneral[key] = value
            elif self.general.get(key) != value:
                self.profileGeneral[key] = value

        msg = 'SETTINGS MANAGER: General settings for profile set'
        debug.printMessage(debug.LEVEL_INFO, msg, True)

    def saveProfileSettings(
        self,
        general: Dict[str, Any],
        pronunciations: Optional[Dict[str, Any]] = None,
        keybindings: Optional[Dict[str, Any]] = None,
    ) -> None:
        profileName: Optional[str] = self.profile
        profileSetting = general.get('profile')
        if isinstance(profileSetting, (list, tuple)) and len(profileSetting) > 1:
            profileName = profileSetting[1]
        elif not profileName:
            defaultProfile = settings.profile
            if isinstance(defaultProfile, (list, tuple)) and len(defaultProfile) > 1:
                profileName = defaultProfile[1]
            else:
                profileName = 'default'

        self.profile = profileName
        generalCopy = dict(general)
        self._ensurePluginPersistenceSettings(generalCopy)
        self._setProfileGeneral(generalCopy)

        if self.profile is None:
            return

        profileName = self.profile
        if pronunciations is None:
            pronunciations = self.getPronunciations(profileName) or {}
        if keybindings is None:
            keybindings = self.getKeybindings(profileName) or {}

        self._setProfilePronunciations(pronunciations)
        self._setProfileKeybindings(keybindings)

        if self._backend:
            self._backend.saveProfileSettings(self.profile,
                                              self.profileGeneral,
                                              self.profilePronunciations,
                                              self.profileKeybindings)

    def _setProfilePronunciations(self, pronunciations: Dict[str, Any]) -> None:
        """Set the changed pronunciations settings from the defaults' ones
        as the profile's."""

        msg = 'SETTINGS MANAGER: Setting pronunciation settings for profile.'
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        self.profilePronunciations = self.defaultPronunciations.copy()
        self.profilePronunciations.update(pronunciations)

        msg = 'SETTINGS MANAGER: Pronunciation settings for profile set.'
        debug.printMessage(debug.LEVEL_INFO, msg, True)

    def _setProfileKeybindings(self, keybindings: Dict[str, Any]) -> None:
        """Set the changed keybindings settings from the defaults' ones
        as the profile's."""

        msg = 'SETTINGS MANAGER: Setting keybindings settings for profile.'
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        self.profileKeybindings = self.defaultKeybindings.copy()
        self.profileKeybindings.update(keybindings)

        msg = 'SETTINGS MANAGER: Keybindings settings for profile set.'
        debug.printMessage(debug.LEVEL_INFO, msg, True)

    def _saveAppSettings(self, appName: str, general: Dict[str, Any], pronunciations: Dict[str, Any], keybindings: Dict[str, Any]) -> None:
        if not self._backend:
            return

        profileScopedKeys = {'activePlugins', 'pluginSources'}
        appGeneral = {}
        profileGeneral = self.getGeneralSettings(self.profile) if self.profile else {}
        for key, value in general.items():
            if key in profileScopedKeys:
                continue
            if value != profileGeneral.get(key):
                appGeneral[key] = value

        appPronunciations = {}
        profilePronunciations = self.getPronunciations(self.profile) if self.profile else {}
        for key, value in pronunciations.items():
            if value != profilePronunciations.get(key):
                appPronunciations[key] = value

        appKeybindings = {}
        profileKeybindings = self.getKeybindings(self.profile) if self.profile else {}
        for key, value in keybindings.items():
            if value != profileKeybindings.get(key):
                appKeybindings[key] = value

        self._backend.saveAppSettings(appName,
                                      self.profile,
                                      appGeneral,
                                      appPronunciations,
                                      appKeybindings)

    def saveSettings(self, script: Script, general: Dict[str, Any], pronunciations: Dict[str, Any], keybindings: Dict[str, Any]) -> Optional[bool]:
        """Save the settings provided for the script provided."""

        tokens = ["SETTINGS MANAGER: Saving settings for", script, "(app:", script.app, ")"]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)
        app = script.app
        if app:
            appName = AXObject.get_name(app)
            if appName:
                self._saveAppSettings(appName, general, pronunciations, keybindings)
                return None

        # Assign current profile
        _profile = general.get('profile', settings.profile)
        currentProfile = _profile[1]

        self.profile = currentProfile
        self._ensurePluginPersistenceSettings(general)

        # Elements that need to stay updated in main configuration.
        self.defaultGeneral['startingProfile'] = general.get('startingProfile',
                                                              _profile)

        self.saveProfileSettings(general, pronunciations, keybindings)

        tokens = ["SETTINGS MANAGER: Saving for backend", self._backend]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)

        tokens = ["SETTINGS MANAGER: Settings for", script, "(app:", script.app, ") saved"]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)
        return self._enableAccessibility()

    def _adjustBindingTupleValues(self, bindingTuple: tuple) -> tuple:
        """Converts the values of bindingTuple into KeyBinding-ready values."""

        keysym, mask, mods, clicks = bindingTuple
        if not keysym:
            bindingTuple = ('', 0, 0, 0)
        else:
            bindingTuple = (keysym, int(mask), int(mods), int(clicks))

        return bindingTuple

    def overrideKeyBindings(self, script: Script, scriptKeyBindings: Any) -> Any:
        keybindingsSettings = self.profileKeybindings
        for handlerString, bindingTuples in keybindingsSettings.items():
            handler: Optional[InputEventHandler] = script.inputEventHandlers.get(handlerString)
            if not handler:
                continue

            scriptKeyBindings.removeByHandler(handler)
            for bindingTuple in bindingTuples:
                bindingTuple = self._adjustBindingTupleValues(bindingTuple)
                keysym, mask, mods, clicks = bindingTuple
                newBinding = KeyBinding(keysym, mask, mods, handler, clicks)
                scriptKeyBindings.add(newBinding)

        return scriptKeyBindings

    def isFirstStart(self) -> bool:
        """Check if the firstStart key is True or false"""
        if self._backend:
            return self._backend.isFirstStart()
        return False

    def setFirstStart(self, value: bool = False) -> None:
        """Set firstStart. This user-configurable setting is primarily
        intended to serve as an indication as to whether or not initial
        configuration is needed."""
        if self._backend:
            self._backend.setFirstStart(value)

    def availableProfiles(self) -> List[str]:
        """Get available profiles from active backend"""
        if self._backend:
            return self._backend.availableProfiles()
        return []

    def getAppSetting(self, app: Any, settingName: str, fallbackOnDefault: bool = True) -> Any:
        if not app or not self._backend:
            return None

        appPrefs = self._backend.getAppSettings(AXObject.get_name(app))
        profiles = appPrefs.get('profiles', {})
        profilePrefs = profiles.get(self.profile, {})
        general = profilePrefs.get('general', {})
        appSetting = general.get(settingName)
        if appSetting is None and fallbackOnDefault and self.profile:
            general = self._backend.getGeneral(self.profile)
            appSetting = general.get(settingName)

        return appSetting

    def loadAppSettings(self, script: Script) -> None:
        """Load the users application specific settings for an app.

        Arguments:
        - script: the current active script.
        """

        if not (script and script.app):
            return

        if not self._backend:
            return

        for key in list(self._appPronunciations.keys()):
            if key in self.pronunciations:
                self.pronunciations.pop(key)

        prefs = self._backend.getAppSettings(AXObject.get_name(script.app))
        profiles = prefs.get('profiles', {})
        profilePrefs = profiles.get(self.profile, {})

        self._appGeneral = profilePrefs.get('general', {})
        self._appKeybindings = profilePrefs.get('keybindings', {})
        self._appPronunciations = profilePrefs.get('pronunciations', {})
        self._activeApp = AXObject.get_name(script.app) or ""

        self._loadProfileSettings()
        self._mergeSettings()
        self._setSettingsRuntime(self.general)
        self._setPronunciationsRuntime(self.pronunciations)
        script.keyBindings = self.overrideKeyBindings(script, script.getKeyBindings())

_managerInstance: Optional[SettingsManager] = None

def getManager() -> Optional[SettingsManager]:
    """Get the settings manager instance. Compatibility function.

    This function provides backward compatibility for existing code that uses
    settings_manager.getManager(). Returns None during initial import phase,
    and the actual instance once cthulhuApp is initialized.

    Code should check for None or call this lazily when actually needed.
    """
    global _managerInstance
    if _managerInstance is None:
        try:
            from . import cthulhu
            if cthulhu.cthulhuApp:
                 _managerInstance = cthulhu.cthulhuApp.settingsManager
        except (ImportError, AttributeError):
            # During import phase, cthulhuApp may not exist yet
            pass
    return _managerInstance
