#!/usr/bin/env python3

# Libervia D-Bus communication bridge
# Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program 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 Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import asyncio
from functools import partial

from twisted.internet import reactor
from txdbus import client

from .bridge_frontend import BridgeException
from libervia.backend.core.exceptions import BridgeExceptionNoService, BridgeInitError
from libervia.backend.core.i18n import _
from libervia.backend.core.log import getLogger
from libervia.backend.tools import config

log = getLogger(__name__)


# Interface prefix
conf = config.parse_main_conf()
bridge_conf = conf.get_bridge_conf(config.DBusBridgeSettings)
const_INT_PREFIX = bridge_conf.int_prefix
const_ERROR_PREFIX = const_INT_PREFIX + ".error"
const_OBJ_PATH = "/org/libervia/Libervia/bridge"
const_CORE_SUFFIX = ".core"
const_PLUGIN_SUFFIX = ".plugin"
const_TIMEOUT = 120


def dbus_to_bridge_exception(failure):
    """Convert a txdbus Failure to a BridgeException.

    @param failure (Failure or RemoteError)
    @return: BridgeException
    """
    from twisted.python.failure import Failure

    if isinstance(failure, Failure):
        err = failure.value
        message = failure.getErrorMessage()
    else:
        err = failure
        message = str(failure)

    full_name = getattr(err, "dbusErrorName", None) or getattr(err, "errName", None)
    if full_name is None:
        full_name = ".".join(
            (const_ERROR_PREFIX, err.__class__.__module__, err.__class__.__name__)
        )

    if full_name and full_name.startswith(const_ERROR_PREFIX):
        name = full_name[len(const_ERROR_PREFIX) + 1 :]
    else:
        name = full_name

    return BridgeException(name, message, "")


class SignalsHandler:

    def __init__(self, db_object, iface_name):
        self.db_object = db_object
        self._iface_names = {
            "core": const_INT_PREFIX + const_CORE_SUFFIX,
            "plugin": iface_name,
        }

    def __getattr__(self, name):
        if name.startswith("remote_"):
            log.debug("calling an unregistered signal: {name}".format(name=name[7:]))
            return lambda *args, **kwargs: None
        else:
            raise AttributeError(name)

    def register_signal(self, name, handler, iface="core"):
        log.debug("registering signal {name}".format(name=name))
        method_name = "remote_" + name
        try:
            self.__getattribute__(method_name)
        except AttributeError:
            pass
        else:
            from libervia.backend.core import exceptions

            raise exceptions.InternalError(
                "{name} signal handler has been registered twice".format(name=method_name)
            )

        def on_signal(*args):
            handler(*args)

        d = self.db_object.notifyOnSignal(
            name, on_signal, interface=self._iface_names[iface]
        )
        d.addErrback(
            lambda failure_: log.error(
                f"Failed to register signal {name}: {failure_.getErrorMessage()}"
            )
        )

        setattr(self, method_name, on_signal)


class bridge:

    def __init__(self):
        self.conn = None
        self.db_object = None
        self.db_core_iface = None
        self.db_plugin_iface = None
        self.signals_handler = None

    def __getattr__(self, name):
        return partial(self.call, name)

    @staticmethod
    def _unwrap_txdbus_result(d, return_sig):
        """Unwrap txdbus result."""
        # txdbus seems to wrongly wrap result in a list, this is a workaround.
        # FIXME: check txdbus behaviour, and report upstream if it's confirmed to be a
        # bug.
        if return_sig and return_sig.startswith("("):
            d.addCallback(
                lambda result: (
                    result[0] if isinstance(result, list) and len(result) == 1 else result
                )
            )
        return d

    def call(self, name: str, *args, **kwargs):
        """Call a remote method.

        @param name: name of the bridge method
        @param args: method arguments
        @param kwargs: keyword arguments (callback, errback, and methods arguments)
        """
        # Determine which interface to use by checking where the method is defined
        for iface in self.db_object.interfaces:
            if name in iface.methods:
                interface_name = iface.name
                return_sig = iface.methods[name].sigOut
                break
        else:
            # Default to core interface if method not found (will raise AttributeError)
            interface_name = const_INT_PREFIX + const_CORE_SUFFIX
            return_sig = None

        # Call the remote method
        d = self.db_object.callRemote(
            name,
            *args,
            interface=interface_name,
            timeout=const_TIMEOUT,
        )

        self._unwrap_txdbus_result(d, return_sig)

        return d

    async def bridge_connect(self) -> None:
        """Connect to the D-Bus bridge."""
        try:
            self.conn = await client.connect(reactor)
            self.db_object = await self.conn.getRemoteObject(
                const_INT_PREFIX, const_OBJ_PATH
            )
            # Find the core and plugin interfaces
            for iface in self.db_object.interfaces:
                if iface.name == const_INT_PREFIX + const_CORE_SUFFIX:
                    self.db_core_iface = iface
                elif iface.name == const_INT_PREFIX + const_PLUGIN_SUFFIX:
                    self.db_plugin_iface = iface

            if self.db_core_iface is None:
                raise BridgeInitError(
                    f"Core interface {const_INT_PREFIX + const_CORE_SUFFIX} not found"
                )
            if self.db_plugin_iface is None:
                raise BridgeInitError(
                    f"Plugin interface {const_INT_PREFIX + const_PLUGIN_SUFFIX} not found"
                )

            # Initialize signals handler
            self.signals_handler = SignalsHandler(
                self.db_object, const_INT_PREFIX + const_PLUGIN_SUFFIX
            )
        except BridgeInitError as e:
            raise e
        except Exception as e:
            msg = str(e)
            if (
                "org.freedesktop.DBus.Error.ServiceUnknown" in msg
                or "org.freedesktop.DBus.Error.Spawn.ExecFailed" in msg
            ):
                raise BridgeExceptionNoService from e
            elif "org.freedesktop.DBus.Error.NotSupported" in msg:
                log.error(
                    _(
                        "D-Bus is not launched, please see README to see instructions on"
                        " how to launch it"
                    )
                )
            raise BridgeInitError from e

    def register_signal(self, functionName, handler, iface="core"):
        if self.signals_handler is None:
            log.error("Bridge not initialized")
            return
        self.signals_handler.register_signal(functionName, handler, iface)

    def action_launch(self, callback_id, data, profile_key="@DEFAULT@"):
        return self.call("action_launch", callback_id, data, profile_key)

    def actions_get(self, profile_key="@DEFAULT@"):
        return self.call("actions_get", profile_key)

    def config_get(self, section, name):
        return self.call("config_get", section, name)

    def connect(self, profile_key="@DEFAULT@", password="", options={}):
        return self.call("connect", profile_key, password, options)

    def contact_add(self, entity_jid, profile_key="@DEFAULT@"):
        return self.call("contact_add", entity_jid, profile_key)

    def contact_del(self, entity_jid, profile_key="@DEFAULT@"):
        return self.call("contact_del", entity_jid, profile_key)

    def contact_get(self, arg_0, profile_key="@DEFAULT@"):
        return self.call("contact_get", arg_0, profile_key)

    def contact_update(self, entity_jid, name, groups, profile_key="@DEFAULT@"):
        return self.call("contact_update", entity_jid, name, groups, profile_key)

    def contacts_get(self, profile_key="@DEFAULT@"):
        return self.call("contacts_get", profile_key)

    def contacts_get_from_group(self, group, profile_key="@DEFAULT@"):
        return self.call("contacts_get_from_group", group, profile_key)

    def devices_infos_get(self, bare_jid, profile_key):
        return self.call("devices_infos_get", bare_jid, profile_key)

    def disco_find_by_features(
        self,
        namespaces,
        identities,
        bare_jid=False,
        service=True,
        roster=True,
        own_jid=True,
        local_device=False,
        profile_key="@DEFAULT@",
    ):
        return self.call(
            "disco_find_by_features",
            namespaces,
            identities,
            bare_jid,
            service,
            roster,
            own_jid,
            local_device,
            profile_key,
        )

    def disco_infos(self, entity_jid, node="", use_cache=True, profile_key="@DEFAULT@"):
        return self.call("disco_infos", entity_jid, node, use_cache, profile_key)

    def disco_items(self, entity_jid, node="", use_cache=True, profile_key="@DEFAULT@"):
        return self.call("disco_items", entity_jid, node, use_cache, profile_key)

    def disconnect(self, profile_key="@DEFAULT@"):
        return self.call("disconnect", profile_key)

    def encryption_namespace_get(self, arg_0):
        return self.call("encryption_namespace_get", arg_0)

    def encryption_plugins_get(
        self,
    ):
        return self.call(
            "encryption_plugins_get",
        )

    def encryption_trust_ui_get(self, to_jid, namespace, profile_key):
        return self.call("encryption_trust_ui_get", to_jid, namespace, profile_key)

    def entities_data_get(self, jids, keys, profile):
        return self.call("entities_data_get", jids, keys, profile)

    def entity_data_get(self, jid, keys, profile):
        return self.call("entity_data_get", jid, keys, profile)

    def features_get(self, profile_key):
        return self.call("features_get", profile_key)

    def history_get(
        self, from_jid, to_jid, limit, between=True, filters="", profile="@NONE@"
    ):
        return self.call(
            "history_get", from_jid, to_jid, limit, between, filters, profile
        )

    def image_check(self, arg_0):
        return self.call("image_check", arg_0)

    def image_convert(self, source, dest, arg_2, extra):
        return self.call("image_convert", source, dest, arg_2, extra)

    def image_generate_preview(self, image_path, profile_key):
        return self.call("image_generate_preview", image_path, profile_key)

    def image_resize(self, image_path, width, height):
        return self.call("image_resize", image_path, width, height)

    def init_pre_script(
        self,
    ):
        return self.call(
            "init_pre_script",
        )

    def is_connected(self, profile_key="@DEFAULT@"):
        return self.call("is_connected", profile_key)

    def main_resource_get(self, contact_jid, profile_key="@DEFAULT@"):
        return self.call("main_resource_get", contact_jid, profile_key)

    def menu_help_get(self, menu_id, language):
        return self.call("menu_help_get", menu_id, language)

    def menu_launch(self, menu_type, path, data, security_limit, profile_key):
        return self.call(
            "menu_launch", menu_type, path, data, security_limit, profile_key
        )

    def menus_get(self, language, security_limit):
        return self.call("menus_get", language, security_limit)

    def message_encryption_get(self, to_jid, profile_key):
        return self.call("message_encryption_get", to_jid, profile_key)

    def message_encryption_start(
        self, to_jid, namespace="", replace=False, profile_key="@NONE@"
    ):
        return self.call(
            "message_encryption_start", to_jid, namespace, replace, profile_key
        )

    def message_encryption_stop(self, to_jid, profile_key):
        return self.call("message_encryption_stop", to_jid, profile_key)

    def message_send(
        self,
        to_jid,
        message,
        subject={},
        mess_type="auto",
        extra={},
        profile_key="@NONE@",
    ):
        return self.call(
            "message_send", to_jid, message, subject, mess_type, extra, profile_key
        )

    def namespaces_get(
        self,
    ):
        return self.call(
            "namespaces_get",
        )

    def notification_add(
        self,
        type_,
        body_plain,
        body_rich,
        title,
        is_global,
        requires_action,
        arg_6,
        priority,
        expire_at,
        extra,
    ):
        return self.call(
            "notification_add",
            type_,
            body_plain,
            body_rich,
            title,
            is_global,
            requires_action,
            arg_6,
            priority,
            expire_at,
            extra,
        )

    def notification_delete(self, id_, is_global, profile_key):
        return self.call("notification_delete", id_, is_global, profile_key)

    def notifications_expired_clean(self, limit_timestamp, profile_key):
        return self.call("notifications_expired_clean", limit_timestamp, profile_key)

    def notifications_get(self, filters, profile_key):
        return self.call("notifications_get", filters, profile_key)

    def param_get_a(self, name, category, attribute="value", profile_key="@DEFAULT@"):
        return self.call("param_get_a", name, category, attribute, profile_key)

    def param_get_a_async(
        self,
        name,
        category,
        attribute="value",
        security_limit=-1,
        profile_key="@DEFAULT@",
    ):
        return self.call(
            "param_get_a_async", name, category, attribute, security_limit, profile_key
        )

    def param_set(
        self, name, value, category, security_limit=-1, profile_key="@DEFAULT@"
    ):
        return self.call("param_set", name, value, category, security_limit, profile_key)

    def param_ui_get(self, security_limit=-1, app="", extra="", profile_key="@DEFAULT@"):
        return self.call("param_ui_get", security_limit, app, extra, profile_key)

    def params_categories_get(
        self,
    ):
        return self.call(
            "params_categories_get",
        )

    def params_register_app(self, xml, security_limit=-1, app=""):
        return self.call("params_register_app", xml, security_limit, app)

    def params_template_load(self, filename):
        return self.call("params_template_load", filename)

    def params_template_save(self, filename):
        return self.call("params_template_save", filename)

    def params_values_from_category_get_async(
        self, category, security_limit=-1, app="", extra="", profile_key="@DEFAULT@"
    ):
        return self.call(
            "params_values_from_category_get_async",
            category,
            security_limit,
            app,
            extra,
            profile_key,
        )

    def presence_set(self, to_jid="", show="", statuses={}, profile_key="@DEFAULT@"):
        return self.call("presence_set", to_jid, show, statuses, profile_key)

    def presence_statuses_get(self, profile_key="@DEFAULT@"):
        return self.call("presence_statuses_get", profile_key)

    def private_data_delete(self, namespace, key, arg_2):
        return self.call("private_data_delete", namespace, key, arg_2)

    def private_data_get(self, namespace, key, profile_key):
        return self.call("private_data_get", namespace, key, profile_key)

    def private_data_set(self, namespace, key, data, profile_key):
        return self.call("private_data_set", namespace, key, data, profile_key)

    def profile_create(self, profile, password="", component=""):
        return self.call("profile_create", profile, password, component)

    def profile_delete_async(self, profile):
        return self.call("profile_delete_async", profile)

    def profile_is_session_started(self, profile_key="@DEFAULT@"):
        return self.call("profile_is_session_started", profile_key)

    def profile_name_get(self, profile_key="@DEFAULT@"):
        return self.call("profile_name_get", profile_key)

    def profile_set_default(self, profile):
        return self.call("profile_set_default", profile)

    def profile_start_session(self, password="", profile_key="@DEFAULT@"):
        return self.call("profile_start_session", password, profile_key)

    def profiles_list_get(self, clients=True, components=False):
        return self.call("profiles_list_get", clients, components)

    def progress_get(self, id, profile):
        return self.call("progress_get", id, profile)

    def progress_get_all(self, profile):
        return self.call("progress_get_all", profile)

    def progress_get_all_metadata(self, profile):
        return self.call("progress_get_all_metadata", profile)

    def ready_get(
        self,
    ):
        return self.call(
            "ready_get",
        )

    def roster_resync(self, profile_key="@DEFAULT@"):
        return self.call("roster_resync", profile_key)

    def session_infos_get(self, profile_key):
        return self.call("session_infos_get", profile_key)

    def sub_waiting_get(self, profile_key="@DEFAULT@"):
        return self.call("sub_waiting_get", profile_key)

    def subscription(self, sub_type, entity, profile_key="@DEFAULT@"):
        return self.call("subscription", sub_type, entity, profile_key)

    def tor_infos_get(self, extra, profile_key):
        return self.call("tor_infos_get", extra, profile_key)

    def tor_mode_get(self, profile_key):
        return self.call("tor_mode_get", profile_key)

    def version_get(
        self,
    ):
        return self.call(
            "version_get",
        )


class AIOSignalsHandler(SignalsHandler):

    def register_signal(self, name, handler, iface="core"):
        """Register an async signal handler."""
        async_handler = lambda *args: asyncio.ensure_future(handler(*args))
        return super().register_signal(name, async_handler, iface)

    async def action_launch(self, callback_id, data, profile_key="@DEFAULT@"):
        return await self.call("action_launch", callback_id, data, profile_key)

    async def actions_get(self, profile_key="@DEFAULT@"):
        return await self.call("actions_get", profile_key)

    async def config_get(self, section, name):
        return await self.call("config_get", section, name)

    async def connect(self, profile_key="@DEFAULT@", password="", options={}):
        return await self.call("connect", profile_key, password, options)

    async def contact_add(self, entity_jid, profile_key="@DEFAULT@"):
        return await self.call("contact_add", entity_jid, profile_key)

    async def contact_del(self, entity_jid, profile_key="@DEFAULT@"):
        return await self.call("contact_del", entity_jid, profile_key)

    async def contact_get(self, arg_0, profile_key="@DEFAULT@"):
        return await self.call("contact_get", arg_0, profile_key)

    async def contact_update(self, entity_jid, name, groups, profile_key="@DEFAULT@"):
        return await self.call("contact_update", entity_jid, name, groups, profile_key)

    async def contacts_get(self, profile_key="@DEFAULT@"):
        return await self.call("contacts_get", profile_key)

    async def contacts_get_from_group(self, group, profile_key="@DEFAULT@"):
        return await self.call("contacts_get_from_group", group, profile_key)

    async def devices_infos_get(self, bare_jid, profile_key):
        return await self.call("devices_infos_get", bare_jid, profile_key)

    async def disco_find_by_features(
        self,
        namespaces,
        identities,
        bare_jid=False,
        service=True,
        roster=True,
        own_jid=True,
        local_device=False,
        profile_key="@DEFAULT@",
    ):
        return await self.call(
            "disco_find_by_features",
            namespaces,
            identities,
            bare_jid,
            service,
            roster,
            own_jid,
            local_device,
            profile_key,
        )

    async def disco_infos(
        self, entity_jid, node="", use_cache=True, profile_key="@DEFAULT@"
    ):
        return await self.call("disco_infos", entity_jid, node, use_cache, profile_key)

    async def disco_items(
        self, entity_jid, node="", use_cache=True, profile_key="@DEFAULT@"
    ):
        return await self.call("disco_items", entity_jid, node, use_cache, profile_key)

    async def disconnect(self, profile_key="@DEFAULT@"):
        return await self.call("disconnect", profile_key)

    async def encryption_namespace_get(self, arg_0):
        return await self.call("encryption_namespace_get", arg_0)

    async def encryption_plugins_get(
        self,
    ):
        return await self.call(
            "encryption_plugins_get",
        )

    async def encryption_trust_ui_get(self, to_jid, namespace, profile_key):
        return await self.call("encryption_trust_ui_get", to_jid, namespace, profile_key)

    async def entities_data_get(self, jids, keys, profile):
        return await self.call("entities_data_get", jids, keys, profile)

    async def entity_data_get(self, jid, keys, profile):
        return await self.call("entity_data_get", jid, keys, profile)

    async def features_get(self, profile_key):
        return await self.call("features_get", profile_key)

    async def history_get(
        self, from_jid, to_jid, limit, between=True, filters="", profile="@NONE@"
    ):
        return await self.call(
            "history_get", from_jid, to_jid, limit, between, filters, profile
        )

    async def image_check(self, arg_0):
        return await self.call("image_check", arg_0)

    async def image_convert(self, source, dest, arg_2, extra):
        return await self.call("image_convert", source, dest, arg_2, extra)

    async def image_generate_preview(self, image_path, profile_key):
        return await self.call("image_generate_preview", image_path, profile_key)

    async def image_resize(self, image_path, width, height):
        return await self.call("image_resize", image_path, width, height)

    async def init_pre_script(
        self,
    ):
        return await self.call(
            "init_pre_script",
        )

    async def is_connected(self, profile_key="@DEFAULT@"):
        return await self.call("is_connected", profile_key)

    async def main_resource_get(self, contact_jid, profile_key="@DEFAULT@"):
        return await self.call("main_resource_get", contact_jid, profile_key)

    async def menu_help_get(self, menu_id, language):
        return await self.call("menu_help_get", menu_id, language)

    async def menu_launch(self, menu_type, path, data, security_limit, profile_key):
        return await self.call(
            "menu_launch", menu_type, path, data, security_limit, profile_key
        )

    async def menus_get(self, language, security_limit):
        return await self.call("menus_get", language, security_limit)

    async def message_encryption_get(self, to_jid, profile_key):
        return await self.call("message_encryption_get", to_jid, profile_key)

    async def message_encryption_start(
        self, to_jid, namespace="", replace=False, profile_key="@NONE@"
    ):
        return await self.call(
            "message_encryption_start", to_jid, namespace, replace, profile_key
        )

    async def message_encryption_stop(self, to_jid, profile_key):
        return await self.call("message_encryption_stop", to_jid, profile_key)

    async def message_send(
        self,
        to_jid,
        message,
        subject={},
        mess_type="auto",
        extra={},
        profile_key="@NONE@",
    ):
        return await self.call(
            "message_send", to_jid, message, subject, mess_type, extra, profile_key
        )

    async def namespaces_get(
        self,
    ):
        return await self.call(
            "namespaces_get",
        )

    async def notification_add(
        self,
        type_,
        body_plain,
        body_rich,
        title,
        is_global,
        requires_action,
        arg_6,
        priority,
        expire_at,
        extra,
    ):
        return await self.call(
            "notification_add",
            type_,
            body_plain,
            body_rich,
            title,
            is_global,
            requires_action,
            arg_6,
            priority,
            expire_at,
            extra,
        )

    async def notification_delete(self, id_, is_global, profile_key):
        return await self.call("notification_delete", id_, is_global, profile_key)

    async def notifications_expired_clean(self, limit_timestamp, profile_key):
        return await self.call(
            "notifications_expired_clean", limit_timestamp, profile_key
        )

    async def notifications_get(self, filters, profile_key):
        return await self.call("notifications_get", filters, profile_key)

    async def param_get_a(
        self, name, category, attribute="value", profile_key="@DEFAULT@"
    ):
        return await self.call("param_get_a", name, category, attribute, profile_key)

    async def param_get_a_async(
        self,
        name,
        category,
        attribute="value",
        security_limit=-1,
        profile_key="@DEFAULT@",
    ):
        return await self.call(
            "param_get_a_async", name, category, attribute, security_limit, profile_key
        )

    async def param_set(
        self, name, value, category, security_limit=-1, profile_key="@DEFAULT@"
    ):
        return await self.call(
            "param_set", name, value, category, security_limit, profile_key
        )

    async def param_ui_get(
        self, security_limit=-1, app="", extra="", profile_key="@DEFAULT@"
    ):
        return await self.call("param_ui_get", security_limit, app, extra, profile_key)

    async def params_categories_get(
        self,
    ):
        return await self.call(
            "params_categories_get",
        )

    async def params_register_app(self, xml, security_limit=-1, app=""):
        return await self.call("params_register_app", xml, security_limit, app)

    async def params_template_load(self, filename):
        return await self.call("params_template_load", filename)

    async def params_template_save(self, filename):
        return await self.call("params_template_save", filename)

    async def params_values_from_category_get_async(
        self, category, security_limit=-1, app="", extra="", profile_key="@DEFAULT@"
    ):
        return await self.call(
            "params_values_from_category_get_async",
            category,
            security_limit,
            app,
            extra,
            profile_key,
        )

    async def presence_set(
        self, to_jid="", show="", statuses={}, profile_key="@DEFAULT@"
    ):
        return await self.call("presence_set", to_jid, show, statuses, profile_key)

    async def presence_statuses_get(self, profile_key="@DEFAULT@"):
        return await self.call("presence_statuses_get", profile_key)

    async def private_data_delete(self, namespace, key, arg_2):
        return await self.call("private_data_delete", namespace, key, arg_2)

    async def private_data_get(self, namespace, key, profile_key):
        return await self.call("private_data_get", namespace, key, profile_key)

    async def private_data_set(self, namespace, key, data, profile_key):
        return await self.call("private_data_set", namespace, key, data, profile_key)

    async def profile_create(self, profile, password="", component=""):
        return await self.call("profile_create", profile, password, component)

    async def profile_delete_async(self, profile):
        return await self.call("profile_delete_async", profile)

    async def profile_is_session_started(self, profile_key="@DEFAULT@"):
        return await self.call("profile_is_session_started", profile_key)

    async def profile_name_get(self, profile_key="@DEFAULT@"):
        return await self.call("profile_name_get", profile_key)

    async def profile_set_default(self, profile):
        return await self.call("profile_set_default", profile)

    async def profile_start_session(self, password="", profile_key="@DEFAULT@"):
        return await self.call("profile_start_session", password, profile_key)

    async def profiles_list_get(self, clients=True, components=False):
        return await self.call("profiles_list_get", clients, components)

    async def progress_get(self, id, profile):
        return await self.call("progress_get", id, profile)

    async def progress_get_all(self, profile):
        return await self.call("progress_get_all", profile)

    async def progress_get_all_metadata(self, profile):
        return await self.call("progress_get_all_metadata", profile)

    async def ready_get(
        self,
    ):
        return await self.call(
            "ready_get",
        )

    async def roster_resync(self, profile_key="@DEFAULT@"):
        return await self.call("roster_resync", profile_key)

    async def session_infos_get(self, profile_key):
        return await self.call("session_infos_get", profile_key)

    async def sub_waiting_get(self, profile_key="@DEFAULT@"):
        return await self.call("sub_waiting_get", profile_key)

    async def subscription(self, sub_type, entity, profile_key="@DEFAULT@"):
        return await self.call("subscription", sub_type, entity, profile_key)

    async def tor_infos_get(self, extra, profile_key):
        return await self.call("tor_infos_get", extra, profile_key)

    async def tor_mode_get(self, profile_key):
        return await self.call("tor_mode_get", profile_key)

    async def version_get(
        self,
    ):
        return await self.call(
            "version_get",
        )


class AIOBridge(bridge):

    def __init__(self):
        super().__init__()
        self.signals_handler = None

    async def post_init(self):
        """Post-initialization hook.

        Actually no post init is needed, but the "__getattr__" return "partial" for
        unknown methods, and this causes "dynamic_import.load_and_init_bridge" to fail.
        """
        pass

    def call(self, name: str, *args, **kwargs):
        """Call a remote method and return an awaitable.

        @param name: name of the bridge method
        """
        d = super().call(name, *args, **kwargs)
        return d.asFuture(asyncio.get_event_loop())

    async def bridge_connect(self):
        """Connect to the D-Bus bridge (async version)."""
        try:
            self.conn = await client.connect(reactor).asFuture(asyncio.get_event_loop())
            self.db_object = await self.conn.getRemoteObject(
                const_INT_PREFIX, const_OBJ_PATH
            ).asFuture(asyncio.get_event_loop())

            # Find the core and plugin interfaces
            for iface in self.db_object.interfaces:
                if iface.name == const_INT_PREFIX + const_CORE_SUFFIX:
                    self.db_core_iface = iface
                elif iface.name == const_INT_PREFIX + const_PLUGIN_SUFFIX:
                    self.db_plugin_iface = iface

            if self.db_core_iface is None:
                raise BridgeInitError(
                    f"Core interface {const_INT_PREFIX + const_CORE_SUFFIX} not found"
                )
            if self.db_plugin_iface is None:
                raise BridgeInitError(
                    f"Plugin interface {const_INT_PREFIX + const_PLUGIN_SUFFIX} not found"
                )

            # Initialize signals handler
            self.signals_handler = AIOSignalsHandler(
                self.db_object, const_INT_PREFIX + const_PLUGIN_SUFFIX
            )
        except BridgeInitError as e:
            raise e
        except Exception as e:
            msg = str(e)
            if (
                "org.freedesktop.DBus.Error.ServiceUnknown" in msg
                or "org.freedesktop.DBus.Error.Spawn.ExecFailed" in msg
            ):
                raise BridgeExceptionNoService from e
            elif "org.freedesktop.DBus.Error.NotSupported" in msg:
                log.error(
                    _(
                        "D-Bus is not launched, please see README to see instructions on"
                        " how to launch it"
                    )
                )
            raise BridgeInitError from e

    def register_signal(self, functionName, handler, iface="core"):
        if self.signals_handler is None:
            log.error("Bridge not initialized")
            return
        self.signals_handler.register_signal(functionName, handler, iface)


class TwistedBridge(bridge):

    async def post_init(self):
        """Post-initialization hook.

        Actually no post init is needed, but the "__getattr__" return "partial" for
        unknown methods, and this causes "dynamic_import.load_and_init_bridge" to fail.
        """
        pass

    def _errback(self, failure_):
        """Convert Failure to BridgeException"""
        raise dbus_to_bridge_exception(failure_)

    def call(self, name, *args, **kwargs):
        """Call a remote method and return a Deferred.

        @param name(str): name of the bridge method
        @param args(list): arguments
        @param kwargs(dict): keyword arguments (profile, etc.)
        """
        return super().call(name, *args, **kwargs)

    async def bridge_connect(self) -> None:
        """Connect to the D-Bus bridge."""
        return await super().bridge_connect()

    def register_signal(self, functionName, handler, iface="core"):
        super().register_signal(functionName, handler, iface)

    def action_launch(self, callback_id, data, profile_key="@DEFAULT@"):
        return self.call("action_launch", callback_id, data, profile_key)

    def actions_get(self, profile_key="@DEFAULT@"):
        return self.call("actions_get", profile_key)

    def config_get(self, section, name):
        return self.call("config_get", section, name)

    def connect(self, profile_key="@DEFAULT@", password="", options={}):
        return self.call("connect", profile_key, password, options)

    def contact_add(self, entity_jid, profile_key="@DEFAULT@"):
        return self.call("contact_add", entity_jid, profile_key)

    def contact_del(self, entity_jid, profile_key="@DEFAULT@"):
        return self.call("contact_del", entity_jid, profile_key)

    def contact_get(self, arg_0, profile_key="@DEFAULT@"):
        return self.call("contact_get", arg_0, profile_key)

    def contact_update(self, entity_jid, name, groups, profile_key="@DEFAULT@"):
        return self.call("contact_update", entity_jid, name, groups, profile_key)

    def contacts_get(self, profile_key="@DEFAULT@"):
        return self.call("contacts_get", profile_key)

    def contacts_get_from_group(self, group, profile_key="@DEFAULT@"):
        return self.call("contacts_get_from_group", group, profile_key)

    def devices_infos_get(self, bare_jid, profile_key):
        return self.call("devices_infos_get", bare_jid, profile_key)

    def disco_find_by_features(
        self,
        namespaces,
        identities,
        bare_jid=False,
        service=True,
        roster=True,
        own_jid=True,
        local_device=False,
        profile_key="@DEFAULT@",
    ):
        return self.call(
            "disco_find_by_features",
            namespaces,
            identities,
            bare_jid,
            service,
            roster,
            own_jid,
            local_device,
            profile_key,
        )

    def disco_infos(self, entity_jid, node="", use_cache=True, profile_key="@DEFAULT@"):
        return self.call("disco_infos", entity_jid, node, use_cache, profile_key)

    def disco_items(self, entity_jid, node="", use_cache=True, profile_key="@DEFAULT@"):
        return self.call("disco_items", entity_jid, node, use_cache, profile_key)

    def disconnect(self, profile_key="@DEFAULT@"):
        return self.call("disconnect", profile_key)

    def encryption_namespace_get(self, arg_0):
        return self.call("encryption_namespace_get", arg_0)

    def encryption_plugins_get(
        self,
    ):
        return self.call(
            "encryption_plugins_get",
        )

    def encryption_trust_ui_get(self, to_jid, namespace, profile_key):
        return self.call("encryption_trust_ui_get", to_jid, namespace, profile_key)

    def entities_data_get(self, jids, keys, profile):
        return self.call("entities_data_get", jids, keys, profile)

    def entity_data_get(self, jid, keys, profile):
        return self.call("entity_data_get", jid, keys, profile)

    def features_get(self, profile_key):
        return self.call("features_get", profile_key)

    def history_get(
        self, from_jid, to_jid, limit, between=True, filters="", profile="@NONE@"
    ):
        return self.call(
            "history_get", from_jid, to_jid, limit, between, filters, profile
        )

    def image_check(self, arg_0):
        return self.call("image_check", arg_0)

    def image_convert(self, source, dest, arg_2, extra):
        return self.call("image_convert", source, dest, arg_2, extra)

    def image_generate_preview(self, image_path, profile_key):
        return self.call("image_generate_preview", image_path, profile_key)

    def image_resize(self, image_path, width, height):
        return self.call("image_resize", image_path, width, height)

    def init_pre_script(
        self,
    ):
        return self.call(
            "init_pre_script",
        )

    def is_connected(self, profile_key="@DEFAULT@"):
        return self.call("is_connected", profile_key)

    def main_resource_get(self, contact_jid, profile_key="@DEFAULT@"):
        return self.call("main_resource_get", contact_jid, profile_key)

    def menu_help_get(self, menu_id, language):
        return self.call("menu_help_get", menu_id, language)

    def menu_launch(self, menu_type, path, data, security_limit, profile_key):
        return self.call(
            "menu_launch", menu_type, path, data, security_limit, profile_key
        )

    def menus_get(self, language, security_limit):
        return self.call("menus_get", language, security_limit)

    def message_encryption_get(self, to_jid, profile_key):
        return self.call("message_encryption_get", to_jid, profile_key)

    def message_encryption_start(
        self, to_jid, namespace="", replace=False, profile_key="@NONE@"
    ):
        return self.call(
            "message_encryption_start", to_jid, namespace, replace, profile_key
        )

    def message_encryption_stop(self, to_jid, profile_key):
        return self.call("message_encryption_stop", to_jid, profile_key)

    def message_send(
        self,
        to_jid,
        message,
        subject={},
        mess_type="auto",
        extra={},
        profile_key="@NONE@",
    ):
        return self.call(
            "message_send", to_jid, message, subject, mess_type, extra, profile_key
        )

    def namespaces_get(
        self,
    ):
        return self.call(
            "namespaces_get",
        )

    def notification_add(
        self,
        type_,
        body_plain,
        body_rich,
        title,
        is_global,
        requires_action,
        arg_6,
        priority,
        expire_at,
        extra,
    ):
        return self.call(
            "notification_add",
            type_,
            body_plain,
            body_rich,
            title,
            is_global,
            requires_action,
            arg_6,
            priority,
            expire_at,
            extra,
        )

    def notification_delete(self, id_, is_global, profile_key):
        return self.call("notification_delete", id_, is_global, profile_key)

    def notifications_expired_clean(self, limit_timestamp, profile_key):
        return self.call("notifications_expired_clean", limit_timestamp, profile_key)

    def notifications_get(self, filters, profile_key):
        return self.call("notifications_get", filters, profile_key)

    def param_get_a(self, name, category, attribute="value", profile_key="@DEFAULT@"):
        return self.call("param_get_a", name, category, attribute, profile_key)

    def param_get_a_async(
        self,
        name,
        category,
        attribute="value",
        security_limit=-1,
        profile_key="@DEFAULT@",
    ):
        return self.call(
            "param_get_a_async", name, category, attribute, security_limit, profile_key
        )

    def param_set(
        self, name, value, category, security_limit=-1, profile_key="@DEFAULT@"
    ):
        return self.call("param_set", name, value, category, security_limit, profile_key)

    def param_ui_get(self, security_limit=-1, app="", extra="", profile_key="@DEFAULT@"):
        return self.call("param_ui_get", security_limit, app, extra, profile_key)

    def params_categories_get(
        self,
    ):
        return self.call(
            "params_categories_get",
        )

    def params_register_app(self, xml, security_limit=-1, app=""):
        return self.call("params_register_app", xml, security_limit, app)

    def params_template_load(self, filename):
        return self.call("params_template_load", filename)

    def params_template_save(self, filename):
        return self.call("params_template_save", filename)

    def params_values_from_category_get_async(
        self, category, security_limit=-1, app="", extra="", profile_key="@DEFAULT@"
    ):
        return self.call(
            "params_values_from_category_get_async",
            category,
            security_limit,
            app,
            extra,
            profile_key,
        )

    def presence_set(self, to_jid="", show="", statuses={}, profile_key="@DEFAULT@"):
        return self.call("presence_set", to_jid, show, statuses, profile_key)

    def presence_statuses_get(self, profile_key="@DEFAULT@"):
        return self.call("presence_statuses_get", profile_key)

    def private_data_delete(self, namespace, key, arg_2):
        return self.call("private_data_delete", namespace, key, arg_2)

    def private_data_get(self, namespace, key, profile_key):
        return self.call("private_data_get", namespace, key, profile_key)

    def private_data_set(self, namespace, key, data, profile_key):
        return self.call("private_data_set", namespace, key, data, profile_key)

    def profile_create(self, profile, password="", component=""):
        return self.call("profile_create", profile, password, component)

    def profile_delete_async(self, profile):
        return self.call("profile_delete_async", profile)

    def profile_is_session_started(self, profile_key="@DEFAULT@"):
        return self.call("profile_is_session_started", profile_key)

    def profile_name_get(self, profile_key="@DEFAULT@"):
        return self.call("profile_name_get", profile_key)

    def profile_set_default(self, profile):
        return self.call("profile_set_default", profile)

    def profile_start_session(self, password="", profile_key="@DEFAULT@"):
        return self.call("profile_start_session", password, profile_key)

    def profiles_list_get(self, clients=True, components=False):
        return self.call("profiles_list_get", clients, components)

    def progress_get(self, id, profile):
        return self.call("progress_get", id, profile)

    def progress_get_all(self, profile):
        return self.call("progress_get_all", profile)

    def progress_get_all_metadata(self, profile):
        return self.call("progress_get_all_metadata", profile)

    def ready_get(
        self,
    ):
        return self.call(
            "ready_get",
        )

    def roster_resync(self, profile_key="@DEFAULT@"):
        return self.call("roster_resync", profile_key)

    def session_infos_get(self, profile_key):
        return self.call("session_infos_get", profile_key)

    def sub_waiting_get(self, profile_key="@DEFAULT@"):
        return self.call("sub_waiting_get", profile_key)

    def subscription(self, sub_type, entity, profile_key="@DEFAULT@"):
        return self.call("subscription", sub_type, entity, profile_key)

    def tor_infos_get(self, extra, profile_key):
        return self.call("tor_infos_get", extra, profile_key)

    def tor_mode_get(self, profile_key):
        return self.call("tor_mode_get", profile_key)

    def version_get(
        self,
    ):
        return self.call(
            "version_get",
        )
