#!/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)


##METHODS_PART##


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_METHODS_PART##


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)


##TWISTED_METHODS_PART##
