"""
Module to manage FreeBSD kernel modules
"""

import os
import re

import salt.utils.files

# Define the module's virtual name
__virtualname__ = "kmod"


_LOAD_MODULE = '{0}_load="YES"'
_LOADER_CONF = "/boot/loader.conf"
_MODULE_RE = '^{0}_load="YES"'
_MODULES_RE = r'^(\w+)_load="YES"'


def __virtual__():
    """
    Only runs on FreeBSD systems
    """
    if __grains__["kernel"] == "FreeBSD":
        return __virtualname__
    return (
        False,
        "The freebsdkmod execution module cannot be loaded: only available on FreeBSD"
        " systems.",
    )


def _new_mods(pre_mods, post_mods):
    """
    Return a list of the new modules, pass an kldstat dict before running
    modprobe and one after modprobe has run
    """
    pre = set()
    post = set()
    for mod in pre_mods:
        pre.add(mod["module"])
    for mod in post_mods:
        post.add(mod["module"])
    return post - pre


def _rm_mods(pre_mods, post_mods):
    """
    Return a list of the new modules, pass an kldstat dict before running
    modprobe and one after modprobe has run
    """
    pre = set()
    post = set()
    for mod in pre_mods:
        pre.add(mod["module"])
    for mod in post_mods:
        post.add(mod["module"])
    return pre - post


def _get_module_name(line):
    match = re.search(_MODULES_RE, line)
    if match:
        return match.group(1)
    return None


def _get_persistent_modules():
    """
    Returns a list of modules in loader.conf that load on boot.
    """
    mods = set()
    with salt.utils.files.fopen(_LOADER_CONF, "r") as loader_conf:
        for line in loader_conf:
            line = salt.utils.stringutils.to_unicode(line)
            line = line.strip()
            mod_name = _get_module_name(line)
            if mod_name:
                mods.add(mod_name)
    return mods


def _set_persistent_module(mod):
    """
    Add a module to loader.conf to make it persistent.
    """
    if not mod or mod in mod_list(True) or mod not in available():
        return set()
    __salt__["file.append"](_LOADER_CONF, _LOAD_MODULE.format(mod))
    return {mod}


def _remove_persistent_module(mod, comment):
    """
    Remove module from loader.conf. If comment is true only comment line where
    module is.
    """
    if not mod or mod not in mod_list(True):
        return set()

    if comment:
        __salt__["file.comment"](_LOADER_CONF, _MODULE_RE.format(mod))
    else:
        __salt__["file.sed"](_LOADER_CONF, _MODULE_RE.format(mod), "")

    return {mod}


def available():
    """
    Return a list of all available kernel modules

    CLI Example:

    .. code-block:: bash

        salt '*' kmod.available
    """
    ret = []
    for path in __salt__["file.find"]("/boot/kernel", name="*.ko$"):
        bpath = os.path.basename(path)
        comps = bpath.split(".")
        if "ko" in comps:
            # This is a kernel module, return it without the .ko extension
            ret.append(".".join(comps[: comps.index("ko")]))
    return ret


def check_available(mod):
    """
    Check to see if the specified kernel module is available

    CLI Example:

    .. code-block:: bash

        salt '*' kmod.check_available vmm
    """
    return mod in available()


def lsmod():
    """
    Return a dict containing information about currently loaded modules

    CLI Example:

    .. code-block:: bash

        salt '*' kmod.lsmod
    """
    ret = []
    for line in __salt__["cmd.run"]("kldstat").splitlines():
        comps = line.split()
        if not len(comps) > 2:
            continue
        if comps[0] == "Id":
            continue
        if comps[4] == "kernel":
            continue
        ret.append({"module": comps[4][:-3], "size": comps[3], "depcount": comps[1]})
    return ret


def mod_list(only_persist=False):
    """
    Return a list of the loaded module names

    CLI Example:

    .. code-block:: bash

        salt '*' kmod.mod_list
    """
    mods = set()
    if only_persist:
        if not _get_persistent_modules():
            return mods
        for mod in _get_persistent_modules():
            mods.add(mod)
    else:
        for mod in lsmod():
            mods.add(mod["module"])
    return sorted(list(mods))


def load(mod, persist=False):
    """
    Load the specified kernel module

    mod
        Name of the module to add

    persist
        Write the module to sysrc kld_modules to make it load on system reboot

    CLI Example:

    .. code-block:: bash

        salt '*' kmod.load bhyve
    """
    pre_mods = lsmod()
    response = __salt__["cmd.run_all"](f"kldload {mod}", python_shell=False)
    if response["retcode"] == 0:
        post_mods = lsmod()
        mods = _new_mods(pre_mods, post_mods)
        persist_mods = set()
        if persist:
            persist_mods = _set_persistent_module(mod)
        return sorted(list(mods | persist_mods))
    elif "module already loaded or in kernel" in response["stderr"]:
        if persist and mod not in _get_persistent_modules():
            persist_mods = _set_persistent_module(mod)
            return sorted(list(persist_mods))
        else:
            # It's compiled into the kernel
            return [None]
    else:
        return f"Module {mod} not found"


def is_loaded(mod):
    """
    Check to see if the specified kernel module is loaded

    CLI Example:

    .. code-block:: bash

        salt '*' kmod.is_loaded vmm
    """
    return mod in mod_list()


def remove(mod, persist=False, comment=True):
    """
    Remove the specified kernel module

    mod
        Name of module to remove

    persist
        Also remove module from /boot/loader.conf

    comment
        If persist is set don't remove line from /boot/loader.conf but only
        comment it

    CLI Example:

    .. code-block:: bash

        salt '*' kmod.remove vmm
    """
    pre_mods = lsmod()
    res = __salt__["cmd.run_all"](f"kldunload {mod}", python_shell=False)
    if res["retcode"] == 0:
        post_mods = lsmod()
        mods = _rm_mods(pre_mods, post_mods)
        persist_mods = set()
        if persist:
            persist_mods = _remove_persistent_module(mod, comment)
        return sorted(list(mods | persist_mods))
    else:
        return "Error removing module {}: {}".format(mod, res["stderr"])
