# Orca
#
# Copyright 2025 Valve Corporation
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# 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.

# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-locals
# pylint: disable=too-many-nested-blocks
# pylint: disable=too-many-arguments
# pylint: disable=too-many-positional-arguments

"""Provides a D-Bus interface for remotely controlling Orca."""

import contextlib
import enum
import inspect
from collections.abc import Callable

from dasbus.connection import SessionMessageBus
from dasbus.error import DBusError
from dasbus.loop import EventLoop
from dasbus.server.interface import dbus_interface
from dasbus.server.publishable import Publishable
from gi.repository import GLib

from . import (  # pylint: disable=no-name-in-module
    debug,
    orca_platform,
)


class HandlerType(enum.Enum):
    """Enumeration of handler types for D-Bus methods."""

    COMMAND = enum.auto()
    PARAMETERIZED_COMMAND = enum.auto()
    GETTER = enum.auto()
    SETTER = enum.auto()


def command(func):
    """Decorator to mark a method as a D-Bus command using its docstring.

    Usage:
        @command
        def toggle_speech(self, script=None, event=None):
            '''Toggles speech on and off.'''
            # method implementation
    """
    description = func.__doc__ or f"D-Bus command: {func.__name__}"
    func.dbus_command_description = description
    return func


def parameterized_command(func):
    """Decorator to mark a method as a D-Bus parameterized command using its docstring.

    Usage:
        @parameterized_command
        def get_voices_for_language(
            self,
            language,
            variant='',
            script=None,
            event=None,
            notify_user=False
        ):
            '''Returns a list of available voices for the specified language.'''
            # method implementation
    """
    description = func.__doc__ or f"D-Bus parameterized command: {func.__name__}"
    func.dbus_parameterized_command_description = description
    return func


def getter(func):
    """Decorator to mark a method as a D-Bus getter using its docstring.

    Usage:
        @getter
        def get_rate(self):
            '''Returns the current speech rate.'''
            # method implementation
    """
    description = func.__doc__ or f"D-Bus getter: {func.__name__}"
    func.dbus_getter_description = description
    return func


def setter(func):
    """Decorator to mark a method as a D-Bus setter using its docstring.

    Usage:
        @setter
        def set_rate(self, value):
            '''Sets the current speech rate.'''
            # method implementation
    """
    description = func.__doc__ or f"D-Bus setter: {func.__name__}"
    func.dbus_setter_description = description
    return func


def _extract_function_parameters(func: Callable) -> list[tuple[str, str]]:
    """Extract parameter names and types from a function signature."""

    sig = inspect.signature(func)
    parameters = []

    skip_params = {"self", "script", "event"}
    for param_name, param in sig.parameters.items():
        if param_name in skip_params:
            continue

        if param.annotation != inspect.Parameter.empty:
            if hasattr(param.annotation, "__name__"):
                type_str = param.annotation.__name__
            else:
                type_str = str(param.annotation).replace("typing.", "")
        else:
            type_str = "Any"
        parameters.append((param_name, type_str))

    return parameters


class _HandlerInfo:
    """Stores processed information about a function exposed via D-Bus."""

    def __init__(
        self,
        python_function_name: str,
        description: str,
        action: Callable[..., bool],
        handler_type: HandlerType = HandlerType.COMMAND,
        parameters: list[tuple[str, str]] | None = None,
    ):
        self.python_function_name: str = python_function_name
        self.description: str = description
        self.action: Callable[..., bool] = action
        self.handler_type: HandlerType = handler_type
        self.parameters: list[tuple[str, str]] = parameters or []


def _sequence_to_variant(result: list | tuple) -> GLib.Variant:
    """Converts a Python list or tuple to a GLib.Variant."""

    homogeneous_signatures: list[tuple[type, str]] = [
        (str, "as"),
        (bool, "ab"),
        (int, "ax"),
    ]
    for element_type, signature in homogeneous_signatures:
        if all(isinstance(x, element_type) for x in result):
            return GLib.Variant(signature, list(result))

    if all(isinstance(x, (list, tuple)) for x in result):
        if not result:
            return GLib.Variant("av", [])
        first_len = len(result[0])
        converted = [tuple(str(item or "") for item in x) for x in result]
        signature = "(" + "s" * first_len + ")"
        return GLib.Variant(f"a{signature}", converted)

    return GLib.Variant("av", [GLib.Variant("v", x) for x in result])


@dbus_interface("org.gnome.Orca.Module")
class OrcaModuleDBusInterface(Publishable):
    """A D-Bus interface representing a specific Orca module (e.g., a manager)."""

    def __init__(self, module_name: str, handlers_info: list[_HandlerInfo]):
        super().__init__()
        self._module_name = module_name
        self._commands: dict[str, _HandlerInfo] = {}
        self._parameterized_commands: dict[str, _HandlerInfo] = {}
        self._getters: dict[str, _HandlerInfo] = {}
        self._setters: dict[str, _HandlerInfo] = {}

        for info in handlers_info:
            handler_type = getattr(info, "handler_type", HandlerType.COMMAND)
            normalized_name = self._normalize_handler_name(info.python_function_name, handler_type)
            if handler_type == HandlerType.GETTER:
                self._getters[normalized_name] = info
            elif handler_type == HandlerType.SETTER:
                self._setters[normalized_name] = info
            elif handler_type == HandlerType.PARAMETERIZED_COMMAND:
                self._parameterized_commands[normalized_name] = info
            else:
                self._commands[normalized_name] = info

        msg = (
            f"DBUS SERVICE: OrcaModuleDBusInterface for {module_name} initialized "
            f"with {len(self._commands)} command(s), "
            f"{len(self._parameterized_commands)} parameterized command(s), "
            f"{len(self._getters)} getter(s), {len(self._setters)} setter(s)."
        )
        debug.print_message(debug.LEVEL_INFO, msg, True)

    def ExecuteRuntimeGetter(self, getter_name: str) -> GLib.Variant:  # pylint: disable=invalid-name
        """Executes the named getter returning the value as a GLib.Variant for D-Bus marshalling."""

        handler_info = self._getters.get(getter_name)
        if not handler_info:
            msg = f"DBUS SERVICE: Unknown getter '{getter_name}' for '{self._module_name}'."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return GLib.Variant("v", GLib.Variant("s", ""))

        result = handler_info.action()
        msg = f"DBUS SERVICE: Getter '{getter_name}' returned: {result}"
        debug.print_message(debug.LEVEL_INFO, msg, True)
        return self._to_variant(result)

    def ExecuteRuntimeSetter(self, setter_name: str, value: GLib.Variant) -> bool:  # pylint: disable=invalid-name
        """Executes the named setter, returning True if succeeded."""

        handler_info = self._setters.get(setter_name)
        if handler_info is None:
            msg = f"DBUS SERVICE: Unknown setter '{setter_name}' for '{self._module_name}'."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        unpacked = value.unpack()
        result = handler_info.action(unpacked)
        msg = f"DBUS SERVICE: Setter '{setter_name}' with value '{unpacked}' returned: {result}"
        debug.print_message(debug.LEVEL_INFO, msg, True)
        return result

    def ListCommands(self) -> list[tuple[str, str]]:  # pylint: disable=invalid-name
        """Returns a list of (command_name, description) for this module (commands only)."""

        command_list = []
        for camel_case_name, info in self._commands.items():
            command_list.append((camel_case_name, info.description))
        return command_list

    def ListParameterizedCommands(  # pylint: disable=invalid-name
        self,
    ) -> list[tuple[str, str, list[tuple[str, str]]]]:
        """Returns a list of (command_name, description, parameters) for this module."""

        command_list = []
        for camel_case_name, info in self._parameterized_commands.items():
            command_list.append((camel_case_name, info.description, info.parameters))
        return command_list

    def ListRuntimeGetters(self) -> list[tuple[str, str]]:  # pylint: disable=invalid-name
        """Returns a list of (getter_name, description) for this module."""

        getter_list = []
        for camel_case_name, info in self._getters.items():
            getter_list.append((camel_case_name, info.description))
        return getter_list

    def ListRuntimeSetters(self) -> list[tuple[str, str]]:  # pylint: disable=invalid-name
        """Returns a list of (setter_name, description) for this module."""

        setter_list = []
        for camel_case_name, info in self._setters.items():
            setter_list.append((camel_case_name, info.description))
        return setter_list

    def ExecuteCommand(self, command_name: str, notify_user: bool) -> bool:  # pylint: disable=invalid-name
        """Executes the named command and returns True if the command succeeded."""

        if command_name not in self._commands:
            msg = f"DBUS SERVICE: Unknown command '{command_name}' for '{self._module_name}'."
            debug.print_message(debug.LEVEL_WARNING, msg, True)
            return False

        handler_info = self._commands[command_name]
        msg = f"DBUS SERVICE: About to execute '{command_name}' in '{self._module_name}'."
        debug.print_message(debug.LEVEL_INFO, msg, True)
        result = handler_info.action(notify_user)
        msg = (
            f"DBUS SERVICE: '{command_name}' in '{self._module_name}' executed. "
            f"Result: {result}, notify_user: {notify_user}"
        )
        debug.print_message(debug.LEVEL_INFO, msg, True)
        return result

    def ExecuteParameterizedCommand(  # pylint: disable=invalid-name
        self,
        command_name: str,
        parameters: dict[str, GLib.Variant],
        notify_user: bool,
    ) -> GLib.Variant:
        """Executes the named command with parameters and returns the result."""

        handler_info = self._parameterized_commands.get(command_name)
        if not handler_info:
            msg = (
                f"DBUS SERVICE: Unknown parameterized command '{command_name}' for "
                f"'{self._module_name}'."
            )
            debug.print_message(debug.LEVEL_WARNING, msg, True)
            return GLib.Variant("b", False)

        kwargs = {name: variant.unpack() for name, variant in parameters.items()}
        kwargs["notify_user"] = notify_user
        msg = f"DBUS SERVICE: About to execute '{command_name}' in '{self._module_name}'."
        debug.print_message(debug.LEVEL_INFO, msg, True)
        result = handler_info.action(**kwargs)
        msg = f"DBUS SERVICE: '{command_name}' in '{self._module_name}' executed."
        debug.print_message(debug.LEVEL_INFO, msg, True)
        return self._to_variant(result)

    def for_publication(self):
        """Returns the D-Bus interface XML for publication."""

        return self.__dbus_xml__  # pylint: disable=no-member

    @staticmethod
    def _normalize_handler_name(
        function_name: str,
        handler_type: HandlerType = HandlerType.COMMAND,
    ) -> str:
        """Normalizes a Python function name for D-Bus exposure (getter/setter/command)."""

        # Only strip prefixes for getters and setters, not for commands
        if handler_type in (HandlerType.GETTER, HandlerType.SETTER):
            if function_name.startswith(("get_", "set_")):
                function_name = function_name[4:]
        return "".join(word.capitalize() for word in function_name.split("_"))

    @staticmethod
    def _to_variant(result):
        """Converts a Python value to a correctly-typed GLib.Variant for D-Bus marshalling."""

        scalar_signatures: dict[type, str] = {
            bool: "b",
            int: "i",
            float: "d",
            str: "s",
        }
        for scalar_type, signature in scalar_signatures.items():
            if isinstance(result, scalar_type):
                return GLib.Variant(signature, result)

        if isinstance(result, dict):
            return GLib.Variant("a{sv}", {str(k): GLib.Variant("v", v) for k, v in result.items()})
        if isinstance(result, (list, tuple)):
            return _sequence_to_variant(result)
        if result is None:
            return GLib.Variant("v", GLib.Variant("s", ""))
        return GLib.Variant("s", str(result))


@dbus_interface("org.gnome.Orca.Service")
class OrcaDBusServiceInterface(Publishable):
    """Internal D-Bus service object that handles D-Bus specifics."""

    def __init__(self) -> None:
        super().__init__()
        self._registered_modules: set[str] = set()

    def for_publication(self):
        """Returns the D-Bus interface XML for publication."""

        return self.__dbus_xml__  # pylint: disable=no-member

    def add_module_interface(
        self,
        module_name: str,
        handlers_info: list[_HandlerInfo],
        bus: SessionMessageBus,
        object_path_base: str,
    ) -> None:
        """Creates and prepares a D-Bus interface for an Orca module."""

        object_path = f"{object_path_base}/{module_name}"
        if module_name in self._registered_modules:
            msg = f"DBUS SERVICE: Interface {module_name} already registered. Replacing."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            try:
                bus.unpublish_object(object_path)
            except DBusError as e:
                msg = f"DBUS SERVICE: Error unpublishing old interface for {module_name}: {e}"
                debug.print_message(debug.LEVEL_INFO, msg, True)
            self._registered_modules.discard(module_name)
        try:
            module_iface = OrcaModuleDBusInterface(module_name, handlers_info)
            bus.publish_object(object_path, module_iface)
            self._registered_modules.add(module_name)
            msg = f"DBUS SERVICE: Successfully published {module_name} at {object_path}."
            debug.print_message(debug.LEVEL_INFO, msg, True)
        except DBusError as e:
            msg = (
                f"DBUS SERVICE: Failed to create or publish D-Bus interface for "
                f"module {module_name} at {object_path}: {e}"
            )
            debug.print_message(debug.LEVEL_SEVERE, msg, True)

    def remove_module_interface(
        self,
        module_name: str,
        bus: SessionMessageBus,
        object_path_base: str,
    ) -> bool:
        """Removes and unpublishes a D-Bus interface for an Orca module."""

        if module_name not in self._registered_modules:
            msg = f"DBUS SERVICE: Module {module_name} is not registered."
            debug.print_message(debug.LEVEL_WARNING, msg, True)
            return False

        object_path = f"{object_path_base}/{module_name}"
        try:
            bus.unpublish_object(object_path)
            self._registered_modules.discard(module_name)
            msg = f"DBUS SERVICE: Successfully removed {module_name} from {object_path}."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True
        except DBusError as e:
            msg = f"DBUS SERVICE: Error removing interface for {module_name}: {e}"
            debug.print_message(debug.LEVEL_WARNING, msg, True)
            return False

    def ListModules(self) -> list[str]:  # pylint: disable=invalid-name
        """Returns a list of registered module names."""

        return list(self._registered_modules)

    def ListCommands(self) -> list[tuple[str, str]]:  # pylint: disable=invalid-name
        """Returns available commands on the main service interface."""

        commands = []
        for attr_name in dir(self):
            if not attr_name.startswith("_") and attr_name[0].isupper():
                attr = getattr(self, attr_name)
                if callable(attr) and hasattr(attr, "__doc__"):
                    description = (
                        attr.__doc__.strip() if attr.__doc__ else f"Service command: {attr_name}"
                    )
                    commands.append((attr_name, description))

        return sorted(commands)

    def ShowPreferences(self) -> bool:  # pylint: disable=invalid-name
        """Shows Orca's preferences GUI."""

        from . import script_manager  # pylint: disable=import-outside-toplevel

        msg = "DBUS SERVICE: ShowPreferences called."
        debug.print_message(debug.LEVEL_INFO, msg, True)

        manager = script_manager.get_manager()
        script = manager.get_active_script() or manager.get_default_script()
        if script is None:
            msg = "DBUS SERVICE: No script available"
            debug.print_message(debug.LEVEL_WARNING, msg, True)
            return False

        script.show_preferences_gui()
        return True

    def PresentMessage(self, message: str) -> bool:  # pylint: disable=invalid-name
        """Presents message to the user."""

        from . import presentation_manager  # pylint: disable=import-outside-toplevel

        msg = f"DBUS SERVICE: PresentMessage called with: '{message}'"
        debug.print_message(debug.LEVEL_INFO, msg, True)

        presentation_manager.get_manager().present_message(message)
        return True

    def GetVersion(self) -> str:  # pylint: disable=invalid-name
        """Returns Orca's version and revision if available."""

        result = orca_platform.version
        if orca_platform.revision:
            result += f" (rev {orca_platform.revision})"

        msg = f"DBUS SERVICE: GetVersion called, returning: {result}"
        debug.print_message(debug.LEVEL_INFO, msg, True)
        return result

    def Quit(self) -> bool:  # pylint: disable=invalid-name
        """Quits Orca. Returns True if the quit request was accepted."""

        msg = "DBUS SERVICE: Quit called."
        debug.print_message(debug.LEVEL_INFO, msg, True)

        from . import orca  # pylint: disable=import-outside-toplevel

        # orca.shutdown() shuts down the dbus service, so send the response immediately and then
        # do the actual shutdown after a brief delay.
        def _delayed_shutdown():
            orca.shutdown()
            return False

        GLib.timeout_add(100, _delayed_shutdown)
        return True

    def shutdown_service(self, bus: SessionMessageBus, object_path_base: str) -> None:
        """Releases D-Bus resources held by this service and its modules."""

        msg = "DBUS SERVICE: Releasing D-Bus resources for service."
        debug.print_message(debug.LEVEL_INFO, msg, True)

        for module_name in list(self._registered_modules):
            module_object_path = f"{object_path_base}/{module_name}"
            msg = (
                f"DBUS SERVICE: Shutting down and unpublishing module {module_name} "
                f"from main service."
            )
            debug.print_message(debug.LEVEL_INFO, msg, True)
            try:
                bus.unpublish_object(module_object_path)
            except DBusError as e:
                msg = f"DBUS SERVICE: Error unpublishing interface for {module_name}: {e}"
                debug.print_message(debug.LEVEL_INFO, msg, True)
        self._registered_modules.clear()


class OrcaRemoteController:
    """Manages Orca's D-Bus service for remote control."""

    SERVICE_NAME = "org.gnome.Orca.Service"
    OBJECT_PATH = "/org/gnome/Orca/Service"

    def __init__(self) -> None:
        self._dbus_service_interface: OrcaDBusServiceInterface | None = None
        self._is_running: bool = False
        self._bus: SessionMessageBus | None = None
        self._event_loop: EventLoop | None = None
        self._pending_registrations: dict[str, object] = {}
        self._total_commands: int = 0
        self._total_getters: int = 0
        self._total_setters: int = 0
        self._total_modules: int = 0

    def start(self) -> bool:
        """Starts the D-Bus service."""

        if self._is_running:
            msg = "REMOTE CONTROLLER: Start called but service is already running."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        msg = "REMOTE CONTROLLER: Attempting to start D-Bus service."
        debug.print_message(debug.LEVEL_INFO, msg, True)

        try:
            self._bus = SessionMessageBus()
            msg = (
                f"REMOTE CONTROLLER: SessionMessageBus acquired: "
                f"{self._bus.connection.get_unique_name()}"
            )
            debug.print_message(debug.LEVEL_INFO, msg, True)
        except DBusError as e:
            self._bus = None
            msg = f"REMOTE CONTROLLER: Failed to acquire D-Bus session bus: {e}"
            debug.print_message(debug.LEVEL_SEVERE, msg, True)
            return False

        self._dbus_service_interface = OrcaDBusServiceInterface()
        try:
            self._bus.publish_object(self.OBJECT_PATH, self._dbus_service_interface)
            self._bus.register_service(self.SERVICE_NAME)
        except DBusError as e:
            msg = f"REMOTE CONTROLLER: Failed to publish service or request name: {e}"
            debug.print_message(debug.LEVEL_SEVERE, msg, True)
            if self._dbus_service_interface and self._bus:
                with contextlib.suppress(DBusError):
                    self._bus.unpublish_object(self.OBJECT_PATH)
            self._dbus_service_interface = None
            self._bus = None
            return False

        self._is_running = True
        msg = (
            f"REMOTE CONTROLLER: Service started name={self.SERVICE_NAME} path={self.OBJECT_PATH}."
        )
        debug.print_message(debug.LEVEL_INFO, msg, True)
        self._process_pending_registrations()
        self._print_registration_summary()
        return True

    def _process_pending_registrations(self) -> None:
        """Processes any module registrations that were queued before the service was ready."""

        if not self._pending_registrations:
            return

        msg = (
            f"REMOTE CONTROLLER: Processing {len(self._pending_registrations)} "
            f"pending module registrations."
        )
        debug.print_message(debug.LEVEL_INFO, msg, True)
        for module_name, module_instance in self._pending_registrations.items():
            msg = f"REMOTE CONTROLLER: Processing pending registration for {module_name}."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            self._register_decorated_commands_internal(module_name, module_instance)

        self._pending_registrations.clear()

    def register_decorated_module(self, module_name: str, module_instance) -> None:
        """Registers a module's decorated D-Bus commands."""

        if not self._is_running or not self._dbus_service_interface or not self._bus:
            msg = (
                f"REMOTE CONTROLLER: Service not ready; queuing decorated registration "
                f"for {module_name}."
            )
            debug.print_message(debug.LEVEL_INFO, msg, True)
            self._pending_registrations[module_name] = module_instance
            return

        self._register_decorated_commands_internal(module_name, module_instance)

    def _register_decorated_commands_internal(self, module_name: str, module_instance) -> None:
        """Internal method that registers decorated commands from a module instance."""

        if not self._is_running or not self._dbus_service_interface or not self._bus:
            msg = (
                f"REMOTE CONTROLLER: Internal error - _register_decorated_commands_internal "
                f"called for {module_name} but service is not ready."
            )
            debug.print_message(debug.LEVEL_SEVERE, msg, True)
            return

        from . import (  # pylint: disable=import-outside-toplevel
            input_event,
            input_event_manager,
            script_manager,
        )

        handlers_info = []
        commands_count = 0
        getters_count = 0
        setters_count = 0

        for attr_name in dir(module_instance):
            attr = getattr(module_instance, attr_name)
            # Command
            if callable(attr) and hasattr(attr, "dbus_command_description"):
                description = attr.dbus_command_description

                def _create_wrapper(method=attr):
                    def _wrapper(notify_user):
                        event = input_event.RemoteControllerEvent()
                        script = script_manager.get_manager().get_active_script()
                        if script is None:
                            script = script_manager.get_manager().get_default_script()
                        rv = method(script=script, event=event, notify_user=notify_user)
                        # TODO - JD: It probably makes sense to fully process these input
                        # events just like any others, rather than have the caller here.
                        input_event_manager.get_manager().process_remote_controller_event(event)
                        return rv

                    return _wrapper

                handler_info = _HandlerInfo(
                    python_function_name=attr_name,
                    description=description,
                    action=_create_wrapper(),
                    handler_type=HandlerType.COMMAND,
                )
                handlers_info.append(handler_info)
                commands_count += 1
            # Parameterized Command
            elif callable(attr) and hasattr(attr, "dbus_parameterized_command_description"):
                description = attr.dbus_parameterized_command_description

                def _create_parameterized_wrapper(method=attr):
                    def _wrapper(**kwargs):
                        event = input_event.RemoteControllerEvent()
                        script = script_manager.get_manager().get_active_script()
                        if script is None:
                            script = script_manager.get_manager().get_default_script()
                        rv = method(script=script, event=event, **kwargs)
                        # TODO - JD: It probably makes sense to fully process these input
                        # events just like any others, rather than have the caller here.
                        input_event_manager.get_manager().process_remote_controller_event(event)
                        return rv

                    return _wrapper

                handler_info = _HandlerInfo(
                    python_function_name=attr_name,
                    description=description,
                    action=_create_parameterized_wrapper(),
                    handler_type=HandlerType.PARAMETERIZED_COMMAND,
                    parameters=_extract_function_parameters(attr),
                )
                handlers_info.append(handler_info)
                commands_count += 1
            # Getter
            elif callable(attr) and hasattr(attr, "dbus_getter_description"):
                description = attr.dbus_getter_description

                def _create_getter_wrapper(method=attr):
                    def _wrapper(_notify_user=None):
                        return method()

                    return _wrapper

                handler_info = _HandlerInfo(
                    python_function_name=attr_name,
                    description=description,
                    action=_create_getter_wrapper(),
                    handler_type=HandlerType.GETTER,
                )
                handlers_info.append(handler_info)
                getters_count += 1
            # Setter
            elif callable(attr) and hasattr(attr, "dbus_setter_description"):
                description = attr.dbus_setter_description

                def _create_setter_wrapper(method=attr):
                    def _wrapper(value):
                        return method(value)

                    return _wrapper

                handler_info = _HandlerInfo(
                    python_function_name=attr_name,
                    description=description,
                    action=_create_setter_wrapper(),
                    handler_type=HandlerType.SETTER,
                )
                handlers_info.append(handler_info)
                setters_count += 1

        if not handlers_info:
            return

        self._total_commands += commands_count
        self._total_getters += getters_count
        self._total_setters += setters_count
        self._total_modules += 1

        self._dbus_service_interface.add_module_interface(
            module_name,
            handlers_info,
            self._bus,
            self.OBJECT_PATH,
        )
        msg = (
            f"REMOTE CONTROLLER: Successfully registered {len(handlers_info)} "
            f"commands/getters/setters for module {module_name}."
        )
        debug.print_message(debug.LEVEL_INFO, msg, True)

    def deregister_module_commands(self, module_name: str) -> bool:
        """Deregisters D-Bus commands for an Orca module."""

        if module_name in self._pending_registrations:
            msg = f"REMOTE CONTROLLER: Removing pending registration for {module_name}."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            del self._pending_registrations[module_name]
            return True

        if not self._is_running or not self._dbus_service_interface or not self._bus:
            msg = (
                f"REMOTE CONTROLLER: Cannot deregister commands for {module_name}; "
                "service not running or bus not available."
            )
            debug.print_message(debug.LEVEL_WARNING, msg, True)
            return False

        return self._dbus_service_interface.remove_module_interface(
            module_name,
            self._bus,
            self.OBJECT_PATH,
        )

    def shutdown(self) -> None:
        """Shuts down the D-Bus service."""

        if not self._is_running:
            msg = "REMOTE CONTROLLER: Shutdown called but service is not running."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return

        msg = "REMOTE CONTROLLER: Attempting to shut down D-Bus service."
        debug.print_message(debug.LEVEL_INFO, msg, True)
        if self._dbus_service_interface and self._bus:
            self._dbus_service_interface.shutdown_service(self._bus, self.OBJECT_PATH)
            try:
                self._bus.unpublish_object(self.OBJECT_PATH)
            except DBusError as e:
                msg = f"REMOTE CONTROLLER: Error unpublishing main service object: {e}"
                debug.print_message(debug.LEVEL_INFO, msg, True)
            self._dbus_service_interface = None

        if self._bus:
            try:
                self._bus.unregister_service(self.SERVICE_NAME)
            except DBusError as e:
                msg = f"REMOTE CONTROLLER: Error releasing bus name: {e}"
                debug.print_message(debug.LEVEL_INFO, msg, True)
            self._bus.disconnect()
            self._bus = None

        self._is_running = False
        msg = "REMOTE CONTROLLER: D-Bus service shut down."
        debug.print_message(debug.LEVEL_INFO, msg, True)
        self._pending_registrations.clear()
        self._total_commands = 0
        self._total_getters = 0
        self._total_setters = 0
        self._total_modules = 0

    def is_running(self) -> bool:
        """Checks if the D-Bus service is currently running."""

        return self._is_running

    def _count_system_commands(self) -> int:
        """Counts the system-wide D-Bus commands available on the main service interface."""

        if not self._dbus_service_interface:
            return 0

        system_commands = 0
        for attr_name in dir(self._dbus_service_interface):
            if not attr_name.startswith("_") and attr_name[0].isupper():
                attr = getattr(self._dbus_service_interface, attr_name)
                if callable(attr) and hasattr(attr, "__doc__"):
                    system_commands += 1
        return system_commands

    def _print_registration_summary(self) -> None:
        """Prints a summary of all registered D-Bus handlers."""

        system_commands_count = self._count_system_commands()
        total_handlers = self._total_commands + self._total_getters + self._total_setters
        msg = (
            f"REMOTE CONTROLLER: Registration complete. Summary: "
            f"{self._total_modules} modules, "
            f"{self._total_commands} module commands, "
            f"{self._total_getters} module getters, "
            f"{self._total_setters} module setters, "
            f"{system_commands_count} system commands. "
            f"Total handlers: {total_handlers + system_commands_count}."
        )
        debug.print_message(debug.LEVEL_INFO, msg, True)


_remote_controller: OrcaRemoteController = OrcaRemoteController()


def get_remote_controller() -> OrcaRemoteController:
    """Returns the OrcaRemoteController singleton."""

    return _remote_controller
