#
# Copyright (C) 2013  Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.  You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#

"""Module providing thread-safe and mainloop-safe DBus operations."""

import gi
gi.require_version("Gio", "2.0")

from gi.repository import Gio

import os
from pyanaconda.core.glib import GError
from pyanaconda.core.constants import DEFAULT_DBUS_TIMEOUT


class SafeDBusError(Exception):
    """Class for exceptions defined in this module."""

    pass


class DBusCallError(SafeDBusError):
    """Class for the errors related to calling methods over DBus."""

    pass


def get_new_session_connection():
    """
    Get a connection handle for the per-user-login-session message bus.

    !!! RUN THIS EARLY !!! like, before any other threads start. Connections to
    the session bus must be made with the effective UID of the login user,
    which in live installs is not the UID of anaconda. This means we need to
    call seteuid in this method, and doing that after threads have started will
    probably do something weird.

    Live installs use consolehelper to run as root, which sets the original
    UID in $USERHELPER_UID.

    :return: the session connection handle
    :rtype: Gio.DBusConnection
    :raise DBusCallError: if some DBus related error appears
    :raise OSError: if unable to set the effective UID
    """

    old_euid = None
    if "USERHELPER_UID" in os.environ:
        old_euid = os.geteuid()
        os.seteuid(int(os.environ["USERHELPER_UID"]))

    try:
        connection = Gio.DBusConnection.new_for_address_sync(
            Gio.dbus_address_get_for_bus_sync(Gio.BusType.SESSION, None),
            Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT |
            Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION,
            None, None)
    except GError as gerr:
        raise DBusCallError("Unable to connect to session bus: {}".format(gerr)) from gerr
    finally:
        if old_euid is not None:
            os.seteuid(old_euid)

    if connection.is_closed():
        raise DBusCallError("Connection is closed")

    return connection


def call_sync(service, obj_path, iface, method, args, connection):
    """
    Safely call a given method on a given object of a given service over DBus
    passing given arguments. If a connection is given, it is used, otherwise a
    new connection is established. Safely means that it is a synchronous,
    thread-safe call not using any main loop.

    :param service: DBus service to use
    :type service: str
    :param obj_path: object path of the object to call method on
    :type obj_path: str
    :param iface: interface to use
    :type iface: str
    :param method: name of the method to call
    :type method: str
    :param args: arguments to pass to the method
    :type args: GVariant
    :param connection: connection to use
    :type connection: Gio.DBusConnection
    :return: unpacked value returned by the method
    :rtype: tuple with elements that depend on the method
    :raise DBusCallError: if some DBus related error appears

    """
    if connection.is_closed():
        raise DBusCallError("Connection is closed")

    try:
        ret = connection.call_sync(service, obj_path, iface, method, args,
                                   None, Gio.DBusCallFlags.NONE,
                                   DEFAULT_DBUS_TIMEOUT, None)
    except GError as gerr:
        msg = "Failed to call %s method on %s with %s arguments: %s" % (method, obj_path,
                                                                        args, str(gerr))
        raise DBusCallError(msg) from gerr

    if ret is None:
        msg = "No return from %s method on %s with %s arguments" % (method, obj_path, args)
        raise DBusCallError(msg)

    return ret.unpack()
