#
# DBus interface for the network module.
#
# Copyright (C) 2018 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.
#

from pyanaconda.modules.common.constants.services import NETWORK
from dasbus.server.property import emits_properties_changed
from dasbus.typing import *  # pylint: disable=wildcard-import
from pyanaconda.modules.common.base import KickstartModuleInterface
from dasbus.server.interface import dbus_interface, dbus_signal, dbus_class
from pyanaconda.modules.common.containers import TaskContainer
from pyanaconda.modules.common.structures.network import NetworkDeviceInfo, \
    NetworkDeviceConfiguration
from pyanaconda.modules.common.task import TaskInterface


@dbus_class
class NetworkInitializationTaskInterface(TaskInterface):
    """The interface for a network configuration initialization task

    Such a task returns a list of names of the devices the task has affected.
    """

    @staticmethod
    def convert_result(value):
        return get_variant(List[Str], value)


@dbus_interface(NETWORK.interface_name)
class NetworkInterface(KickstartModuleInterface):
    """DBus interface for Network module."""

    def connect_signals(self):
        super().connect_signals()
        self.watch_property("Hostname", self.implementation.hostname_changed)
        self.implementation.current_hostname_changed.connect(self.CurrentHostnameChanged)
        self.watch_property("Connected", self.implementation.connected_changed)
        self.implementation.configurations_changed.connect(self._device_configurations_changed)

    @property
    def Hostname(self) -> Str:
        """Hostname the system will use."""
        return self.implementation.hostname

    @Hostname.setter
    @emits_properties_changed
    def Hostname(self, hostname: Str):
        """Set the hostname.

        Sets the hostname of installed system.

        param hostname: a string with a hostname
        """
        self.implementation.set_hostname(hostname)

    @dbus_signal
    def CurrentHostnameChanged(self, hostname: Str):
        """Signal current hostname change."""
        pass

    def GetCurrentHostname(self) -> Str:
        """Current system hostname."""
        return self.implementation.get_current_hostname()

    def SetCurrentHostname(self, hostname: Str):
        """Set current system hostname.

        Sets the hostname of installer environment.

        param: hostname: a string with a hostname
        """
        self.implementation.set_current_hostname(hostname)

    @property
    def Connected(self) -> Bool:
        """Is the system connected to the network?

        The system is considered to be connected if being in one of the states
        NM_STATE_CONNECTED_LOCAL, NM_STATE_CONNECTED_SITE or NM_STATE_CONNECTED_GLOBAL.
        """
        return self.implementation.connected

    def IsConnecting(self) -> Bool:
        """Is NewtorkManager in connecting state?

        The connecting state can indicate that dhcp configuration is
        in progress.

        The state corresponds to NM_STATE_CONNECTING.

        Internal API used for networking initialization and synchronization.
        To be removed after reworking the synchronization.
        """
        return self.implementation.is_connecting()

    def GetSupportedDevices(self) -> List[Structure]:
        """Get info about existing network devices supported by the module.

        :return: list of objects describing supported devices found on the system
        """
        dev_infos = self.implementation.get_supported_devices()
        return NetworkDeviceInfo.to_structure_list(dev_infos)

    def GetActivatedInterfaces(self) -> List[Str]:
        """Get activated network interfaces.

        Device is considered as activated if it has an active network (NM)
        connection.

        :return: list of names of devices having active network connection
        """
        return self.implementation.get_activated_interfaces()

    def InstallNetworkWithTask(self, overwrite: Bool) -> ObjPath:
        """Install network with an installation task.

        FIXME: does overwrite still apply?
        :param overwrite: overwrite existing configuration
        :return: a DBus path of an installation task
        """
        return TaskContainer.to_object_path(
            self.implementation.install_network_with_task(overwrite)
        )

    def ConfigureHostnameWithTask(self, overwrite: Bool) -> ObjPath:
        """Configure hostname with an installation task.

        FIXME: does overwrite still apply?
        :param overwrite: overwrite existing configuration
        :return: a DBus path of an installation task
        """
        return TaskContainer.to_object_path(
            self.implementation.configure_hostname_with_task(overwrite)
        )

    def ConfigureActivationOnBootWithTask(self, onboot_ifaces: List[Str]) -> ObjPath:
        """Configure automatic activation of devices on system boot.

        1) Specified devices are set to be activated automatically.
        2) Policy set in anaconda configuration (default_on_boot) is applied.

        :param onboot_ifaces: list of network interfaces which should have ONBOOT=yes
        """
        return TaskContainer.to_object_path(
            self.implementation.configure_activation_on_boot_with_task(onboot_ifaces)
        )

    def CreateDeviceConfigurations(self):
        """Create and populate the state of network devices configuration."""
        self.implementation.create_device_configurations()

    def GetDeviceConfigurations(self) -> List[Structure]:
        """Get the state of network devices configuration.

        Contains only configuration of devices supported by Anaconda.

        Returns list of NetworkDeviceConfiguration objects holding
        configuration of a network device.

        For a physical device there is only single NetworkDeviceConfiguration
        object bound to the device name (the mandatory persistent element of
        the object).  The uuid corresponds to the configuration of the device
        for installed system.

        For a virtual device there can be multiple NetworkDeviceConfiguration
        objects, bound to uuid of the device configuration (the mandatory
        persistent element of the object).  The device name is set in the
        object only if there exists respective active device with the
        configuration given by uuid applied.

        Configurations correspond to NetworkManager persistent connections by
        their uuid.
        """
        dev_cfgs = self.implementation.get_device_configurations()
        return NetworkDeviceConfiguration.to_structure_list(dev_cfgs)

    def _device_configurations_changed(self, changes):
        self.DeviceConfigurationChanged([
            (
                NetworkDeviceConfiguration.to_structure(old),
                NetworkDeviceConfiguration.to_structure(new)
            )
            for old, new in changes
        ])

    @dbus_signal
    def DeviceConfigurationChanged(self, changes: List[Tuple[Structure, Structure]]):
        """Signal change of network devices configurations."""
        pass

    def ApplyKickstartWithTask(self) -> ObjPath:
        """Apply kickstart configuration which has not already been applied.

        * activate configurations created in initramfs if --activate is True
        * create configurations for %pre kickstart commands and activate eventually

        :returns: DBus path of the task applying the kickstart
        """
        return TaskContainer.to_object_path(
            self.implementation.apply_kickstart_with_task()
        )

    def DumpMissingConfigFilesWithTask(self) -> ObjPath:
        """Dump missing default config file for wired devices.

        Make sure each supported wired device has config file.

        For default auto connections created by NM upon start (which happens in
        case of missing config file, eg the file was not created in initramfs)
        rename the in-memory connection using device name and dump it into
        config file.

        If default auto connections are turned off by NM configuration (based
        on policy, eg on RHEL or server), the connection will be created by Anaconda
        and dumped into config file.

        The connection id (and consequently config file name) is set to device
        name.

        :returns: DBus path of the task dumping the files
        """
        return TaskContainer.to_object_path(
            self.implementation.dump_missing_config_files_with_task()
        )

    def NetworkDeviceConfigurationChanged(self):
        """Inform module that network device configuration might have changed.

        Therefore kickstart for device configurations should be generated
        from persistent configuration instead of using original kickstart data.
        """
        return self.implementation.network_device_configuration_changed()

    def GetDracutArguments(
        self,
        iface: Str,
        target_ip: Str,
        hostname: Str,
        ibft: Bool
    ) -> List[Str]:
        """Get dracut arguments for the iface and iSCSI target.

        The dracut arguments would activate the iface in initramfs so that the
        iSCSI target can be attached (for example to mount root filesystem).

        :param iface: network interface used to connect to the target
        :param target_ip: IP of the iSCSI target
        :param hostname: static hostname to be configured
        :param ibft: the device should be configured from iBFT
        """
        return self.implementation.get_dracut_arguments(iface, target_ip, hostname, ibft)

    def LogConfigurationState(self, msg_header: Str):
        """Logs the state of network configuration.

        :param msg_header: header of the log messages
        """
        return self.implementation.log_configuration_state(msg_header)
