#
# The iSCSI module
#
# Copyright (C) 2019 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 blivet.iscsi import iscsi

from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.core.signal import Signal
from pyanaconda.core.dbus import DBus
from pyanaconda.modules.common.base import KickstartBaseModule
from pyanaconda.modules.common.constants.objects import ISCSI
from pyanaconda.modules.storage.constants import IscsiInterfacesMode
from pyanaconda.modules.storage.iscsi.discover import ISCSIDiscoverTask, ISCSILoginTask
from pyanaconda.modules.storage.iscsi.iscsi_interface import ISCSIInterface

log = get_module_logger(__name__)


class ISCSIModule(KickstartBaseModule):
    """The iSCSI module."""

    def __init__(self):
        super().__init__()
        self.reload_module()

        self.initiator_changed = Signal()

        self._iscsi_data = []

    def publish(self):
        """Publish the module."""
        DBus.publish_object(ISCSI.object_path, ISCSIInterface(self))

    def is_supported(self):
        """Is this module supported?"""
        return bool(iscsi.available)

    def reload_module(self):
        """Reload the iscsi module."""
        log.debug("Start up the iSCSI module.")
        iscsi.startup()

    @property
    def initiator(self):
        """The iSCSI initiator.

        :return: a name of the initiator
        """
        return iscsi.initiator

    def set_initiator(self, initiator):
        """Set the iSCSI initiator.

        :param initiator: a name of the initiator
        """
        if not iscsi.initiator_set:
            iscsi.initiator = initiator
            self.initiator_changed.emit()
            log.debug("The iSCSI initiator is set to '%s'.", initiator)
        else:
            log.debug("The iSCSI initiator has already been set to '%s'.", iscsi.initiator)

    def can_set_initiator(self):
        """Can the initiator be set?

        Once the initiator name is set it can't be changed.
        """
        return not iscsi.initiator_set

    def get_interface_mode(self):
        """Get the mode of interfaces used for iSCSI operations.

        returns: an instance of IscsiInterfacesMode
        """
        mode = iscsi.mode
        if mode == "none":
            return IscsiInterfacesMode.UNSET
        elif mode == "default":
            return IscsiInterfacesMode.DEFAULT
        elif mode == "bind":
            return IscsiInterfacesMode.IFACENAME
        else:
            log.error("Unknown iSCSI interface mode %s set by blivet, using UNSET", mode)
            return IscsiInterfacesMode.UNSET

    def discover_with_task(self, portal, credentials, interfaces_mode):
        """Discover an iSCSI device.

        :param portal: the portal information
        :param credentials: the iSCSI credentials
        :param interfaces_mode: required mode specified by IscsiInterfacesMode
        :return: a task
        """
        return ISCSIDiscoverTask(portal, credentials, interfaces_mode)

    def login_with_task(self, portal, credentials, node):
        """Login into an iSCSI node discovered on a portal.

        :param portal: the portal information
        :param credentials: the iSCSI credentials
        :param node: the node information
        :return: a task
        """
        return ISCSILoginTask(portal, credentials, node)

    def write_configuration(self):
        """Write the configuration to sysroot."""
        log.debug("Write iSCSI configuration.")
        iscsi.write(conf.target.system_root, None)

    def process_kickstart(self, data):
        """Process the kickstart data."""
        if data.iscsiname.iscsiname:
            self.set_initiator(data.iscsiname.iscsiname)
        self._iscsi_data = data.iscsi.iscsi

    def setup_kickstart(self, data):
        """Setup the kickstart data."""
        data.iscsi.iscsi = self.generate_iscsi_data(data.IscsiData)
        if data.iscsi.iscsi:
            data.iscsiname.iscsiname = self.initiator

    def generate_iscsi_data(self, iscsi_data_class):
        """Generate kickstart data based on original kickstart and attached nodes.

        If all nodes for a target were added by a kickstart command, preserve
        this in generated kickstart (ie do not add particular commands for each
        discovered node).
        """
        iscsi_data_list = list(self._iscsi_data)

        for node in iscsi.active_nodes():
            if node in iscsi.ibft_nodes:
                continue
            iscsi_data = iscsi_data_class()
            self._setup_iscsi_data_from_node(iscsi_data, node)

            for ks_command_data in iscsi_data_list:
                # If there is already a (perhaps more general) command
                # attaching the node, do not add another one
                if (iscsi_data.ipaddr == ks_command_data.ipaddr and
                    (not ks_command_data.target or iscsi_data.target == ks_command_data.target) and
                    iscsi_data.port == ks_command_data.port and
                    iscsi_data.iface == ks_command_data.iface):
                    break
            else:
                iscsi_data_list.append(iscsi_data)

        return iscsi_data_list

    def _setup_iscsi_data_from_node(self, iscsi_data, dev_node):
        """Set up iSCSI data from a device node.

        :param iscsi_data: an instance of iSCSI data
        :param dev_node: a device node NodeInfo object
        """
        iscsi_data.ipaddr = dev_node.address
        iscsi_data.target = dev_node.name
        iscsi_data.port = dev_node.port

        if iscsi.ifaces:
            iscsi_data.iface = iscsi.ifaces[dev_node.iface]

        if dev_node.username and dev_node.password:
            iscsi_data.user = dev_node.username
            iscsi_data.password = dev_node.password

        if dev_node.r_username and dev_node.r_password:
            iscsi_data.user_in = dev_node.r_username
            iscsi_data.password_in = dev_node.r_password

        return iscsi_data

    def get_interface(self, iscsi_iface):
        """Get network interface backing iscsi iface.

        :param iscsi_iface: name of an iscsi interface (eg iface0)
        :returns: specification of interface backing the iscsi iface (eg ens3)
                  or "" if there is none
        """
        return iscsi.ifaces.get(iscsi_iface, "")

    def is_node_from_ibft(self, node):
        """Is the node configured from iBFT table?.

        :param node: the node information
        """
        for ibft_node in iscsi.ibft_nodes:
            if ibft_node.name == node.name and ibft_node.address == node.address \
                    and ibft_node.port == int(node.port) and ibft_node.iface == node.iface:
                return True
        return False

    def get_dracut_arguments(self, node):
        """Get dracut arguments for iSCSI device backed by the node.

        :param node: the node information
        :return: a list of dracut arguments

        FIXME: This is just a temporary method.
        """
        log.debug("Getting dracut arguments for iSCSI node %s", node)

        if self.is_node_from_ibft(node):
            return ["rd.iscsi.firmware"]

        blivet_node = iscsi.get_node(node.name, node.address, node.port, node.iface)

        if not blivet_node:
            log.error("No iSCSI node %s found for device", node)
            return []

        address = blivet_node.address
        # surround ipv6 addresses with []
        if ":" in address:
            address = "[{}]".format(address)

        netroot = "netroot=iscsi:"
        if blivet_node.username and blivet_node.password:
            netroot += "{}:{}".format(blivet_node.username, blivet_node.password)
            if blivet_node.r_username and blivet_node.r_password:
                netroot += ":{}:{}".format(blivet_node.r_username, blivet_node.r_password)

        iface_spec = ""
        interface = self.get_interface(blivet_node.iface) or blivet_node.iface
        if interface != "default":
            iface_spec = ":{}:{}".format(blivet_node.iface, interface)
        netroot += "@{}::{}{}::{}".format(address, blivet_node.port, iface_spec, blivet_node.name)

        initiator = "rd.iscsi.initiator={}".format(self.initiator)

        return [netroot, initiator]
