# Copyright 2016 Cloudbase Solutions Srl
# All Rights Reserved.

import os
import re
import uuid

from oslo_log import log as logging

from coriolis import exception
from coriolis.osmorphing import base
from coriolis import utils

RED_HAT_DISTRO_IDENTIFIER = "Red Hat Enterprise Linux"

LOG = logging.getLogger(__name__)

# NOTE: some constants duplicated for backwards-compatibility:
RELEASE_RHEL = RED_HAT_DISTRO_IDENTIFIER
RELEASE_CENTOS = "CentOS"
RELEASE_FEDORA = "Fedora"


class BaseRedHatMorphingTools(base.BaseLinuxOSMorphingTools):
    BIOS_GRUB_LOCATION = "/boot/grub2"
    UEFI_GRUB_LOCATION = "/boot/efi/EFI/redhat"
    _IFCFG_NM_CONTROLLED_MIN_VERSION = 8

    @classmethod
    def check_os_supported(cls, detected_os_info):
        if detected_os_info['distribution_name'] != (
                RED_HAT_DISTRO_IDENTIFIER):
            return False
        return cls._version_supported_util(
            detected_os_info['release_version'], minimum=7)

    def __init__(self, conn, os_root_dir, os_root_dev,
                 hypervisor, event_manager, detected_os_info,
                 osmorphing_parameters, operation_timeout=None):
        super(
            BaseRedHatMorphingTools, self).__init__(
            conn, os_root_dir, os_root_dev, hypervisor, event_manager,
            detected_os_info, osmorphing_parameters, operation_timeout)

    def disable_predictable_nic_names(self):
        cmd = 'grubby --update-kernel=ALL --args="%s"'
        self._exec_cmd_chroot(cmd % "net.ifnames=0 biosdevname=0")

    def get_update_grub2_command(self):
        location = self._get_grub2_cfg_location()
        return "grub2-mkconfig -o %s" % location

    def _get_grub2_cfg_location(self):
        """Get GRUB2 config location for Red Hat-based distros.

        On RHEL 9.4+ and related distros (Rocky, Alma, etc.), the EFI grub.cfg
        is a wrapper that loads from /boot/grub2/grub.cfg. grub2-mkconfig
        refuses to overwrite the wrapper and requires output to
        /boot/grub2/grub.cfg. Prefer the BIOS path when it exists.
        """
        uefi_cfg = os.path.join(self.UEFI_GRUB_LOCATION, "grub.cfg")
        bios_cfg = os.path.join(self.BIOS_GRUB_LOCATION, "grub.cfg")
        # Prefer /boot/grub2/grub.cfg - on RHEL 9.4+ UEFI, the EFI file is a
        # wrapper and grub2-mkconfig must write to /boot/grub2/grub.cfg
        if self._test_path_chroot(bios_cfg):
            return bios_cfg
        if self._test_path_chroot(uefi_cfg):
            return uefi_cfg
        raise Exception(
            "could not determine grub location."
            " boot partition not mounted?")

    def _has_systemd(self):
        try:
            self._exec_cmd_chroot("rpm -q systemd")
            return True
        except Exception:
            return False

    def _set_dhcp_net_config(self, ifcfgs_ethernet):
        for ifcfg_file, iface_cfg in ifcfgs_ethernet:
            if iface_cfg.get("BOOTPROTO") == "none":
                iface_cfg["BOOTPROTO"] = "dhcp"
                iface_cfg["UUID"] = str(uuid.uuid4())

                if 'IPADDR' in iface_cfg:
                    del iface_cfg['IPADDR']
                if 'GATEWAY' in iface_cfg:
                    del iface_cfg['GATEWAY']
                if 'NETMASK' in iface_cfg:
                    del iface_cfg['NETMASK']
                if 'NETWORK' in iface_cfg:
                    del iface_cfg['NETWORK']

                self._write_config_file(ifcfg_file, iface_cfg)

        network_cfg_file = "etc/sysconfig/network"
        network_cfg = self._read_config_file(network_cfg_file,
                                             check_exists=True)
        if "GATEWAY" in network_cfg:
            del network_cfg["GATEWAY"]
            self._write_config_file(network_cfg_file, network_cfg)

    def _comment_keys_from_ifcfg_files(
            self, keys, interfaces=None, backup_file_suffix=".bak"):
        """ Comments the provided list of keys from all 'ifcfg-*' files.
        Optinally skips ifcfg files for interfaces not listed in 'interfaces'.
        """
        if not interfaces:
            interfaces = []
        scripts_dir = os.path.join(
            self._os_root_dir, self._NETWORK_SCRIPTS_PATH)
        all_ifcfg_files = utils.list_ssh_dir(self._ssh, scripts_dir)
        regex = "^(ifcfg-[a-z0-9]+)$"

        for ifcfgf in all_ifcfg_files:
            if not re.match(regex, ifcfgf):
                LOG.debug(
                    "Skipping ifcfg file with unknown filename '%s'." %
                    ifcfgf)
                continue

            if interfaces and not any([i in ifcfgf for i in interfaces]):
                LOG.debug(
                    "Skipping ifcfg file '%s' as it's not for one of the "
                    "requested interfaces (%s)", ifcfgf, interfaces)
                continue

            fullpath = os.path.join(scripts_dir, ifcfgf)
            for key in keys:
                self._exec_cmd(
                    "sudo sed -i%s -E -e 's/^(%s=.*)$/# \\1/g' %s" % (
                        backup_file_suffix, key, fullpath))
            LOG.debug(
                "Commented all %s references from '%s'" % (
                    keys, fullpath))

    def set_net_config(self, nics_info, dhcp):
        if dhcp:
            nics_info = nics_info or []
            if not nics_info:
                return
            self.disable_predictable_nic_names()
            nmconnection_files = (
                self._get_existing_ethernet_nmconnection_files())
            if nmconnection_files:
                self._write_nmconnection_configs(
                    nics_info, nmconnection_files)
            else:
                self._write_nic_configs(nics_info)
            return

        LOG.info("Setting static IP configuration")
        self._setup_network_preservation(nics_info)

    def get_installed_packages(self):
        cmd = 'rpm -qa --qf "%{NAME}\\n"'
        try:
            self.installed_packages = self._exec_cmd_chroot(cmd).splitlines()
        except exception.CoriolisException:
            LOG.warning("Failed to get installed packages")
            LOG.trace(utils.get_exception_details())
            self.installed_packages = []

    def _yum_install(self, package_names):
        try:
            yum_cmd = 'yum install %s -y %s %s' % (
                " ".join(package_names),
                "--setopt=strict=1",
                "--setopt=skip_missing_names_on_install=0"
            )
            self._exec_cmd_chroot(yum_cmd)
        except exception.CoriolisException as err:
            raise exception.FailedPackageInstallationException(
                package_names=package_names, package_manager='yum',
                error=str(err)) from err

    def _yum_uninstall(self, package_names):
        try:
            for package_name in package_names:
                yum_cmd = 'yum remove %s -y' % package_name
                self._exec_cmd_chroot(yum_cmd)
        except exception.CoriolisException as err:
            raise exception.FailedPackageUninstallationException(
                package_names=package_names, package_manager='yum',
                error=str(err)) from err

    def _yum_clean_all(self):
        try:
            self._exec_cmd_chroot("yum clean all")
        except exception.CoriolisException:
            # On systems with no enabled repos, yum clean fails with
            # "There are no enabled repos". This is non-fatal since
            # there is nothing to clean.
            LOG.warning("yum clean all failed (e.g. no enabled repos), "
                        "continuing. Error: %s", utils.get_exception_details())
        if self._test_path('var/cache/yum'):
            self._exec_cmd_chroot("rm -rf /var/cache/yum")

    def _get_repos_to_enable(self):
        return []

    def enable_repos(self, repo_names):
        """Enable repositories using subscription-manager for
        Red Hat Enterprise Linux.

        This method should be overridden by subclasses that use different
        repository management tools.
        """
        if not repo_names:
            return

        # RHEL uses subscription-manager for all versions
        for repo in repo_names:
            cmd = 'subscription-manager repos --enable=%s' % repo
            try:
                self._exec_cmd_chroot(cmd)
                LOG.info("Enabled repository '%s' using subscription-manager",
                         repo)
            except exception.CoriolisException:
                LOG.warning(f"Failed to enable repository {repo}. "
                            f"Error was: {utils.get_exception_details()}")

    def pre_packages_install(self, package_names):
        super(BaseRedHatMorphingTools, self).pre_packages_install(
            package_names)
        self._yum_clean_all()
        if 'grubby' not in self.installed_packages:
            self._yum_install(['grubby'])
        else:
            LOG.debug("Skipping package 'grubby' as it's already installed")

    def post_packages_install(self, package_names):
        self._configure_cloud_init()
        self._run_dracut()
        self._set_selinux_autorelabel()
        super(BaseRedHatMorphingTools, self).post_packages_install(
            package_names)

    def install_packages(self, package_names):
        repos_to_enable = self._get_repos_to_enable()
        self.enable_repos(repos_to_enable)
        self._yum_install(package_names)

    def uninstall_packages(self, package_names):
        self._yum_uninstall(package_names)

    def _run_dracut(self):
        self._exec_cmd_chroot("dracut --regenerate-all -f")

    def _set_network_nozeroconf_config(self):
        network_cfg_file = "etc/sysconfig/network"
        network_cfg = self._read_config_file(network_cfg_file,
                                             check_exists=True)
        network_cfg["NOZEROCONF"] = "yes"
        self._write_config_file(network_cfg_file, network_cfg)

    def _write_config_file(self, chroot_path, config_data):
        content = self._get_config_file_content(config_data)
        self._write_file_sudo(chroot_path, content)

    def _get_config_file_content(self, config):
        return "%s\n" % "\n".join(
            ['%s="%s"' % (k, v) for k, v in config.items()])
