"""
Service support for RHEL-based systems, including support for both upstart and sysvinit

.. important::
    If you feel that Salt should be using this module to manage services on a
    minion, and it is using a different module (or gives an error similar to
    *'service.start' is not available*), see :ref:`here
    <module-provider-override>`.
"""

import fnmatch
import glob
import logging
import os
import re
import stat

import salt.utils.path

log = logging.getLogger(__name__)

__func_alias__ = {"reload_": "reload"}

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

# Import upstart module if needed
HAS_UPSTART = False
if salt.utils.path.which("initctl"):
    try:
        # Don't re-invent the wheel, import the helper functions from the
        # upstart module.
        from salt.modules.upstart_service import (
            _upstart_disable,
            _upstart_enable,
            _upstart_is_enabled,
        )
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Unable to import helper functions from salt.modules.upstart: %s", exc
        )
    else:
        HAS_UPSTART = True


def __virtual__():
    """
    Only work on select distros which still use Red Hat's /usr/bin/service for
    management of either sysvinit or a hybrid sysvinit/upstart init system.
    """
    # Disable when booted with systemd
    if __utils__["systemd.booted"](__context__):
        return (
            False,
            "The rh_service execution module failed to load: this system was booted"
            " with systemd.",
        )

    # Enable on these platforms only.
    enable = {
        "XenServer",
        "XCP-ng",
        "RedHat",
        "CentOS",
        "ScientificLinux",
        "CloudLinux",
        "Amazon",
        "Fedora",
        "ALT",
        "OEL",
        "SUSE  Enterprise Server",
        "SUSE",
        "McAfee  OS Server",
        "VirtuozzoLinux",
    }
    if __grains__["os"] in enable:

        if __grains__["os"] == "SUSE":
            if str(__grains__["osrelease"]).startswith("11"):
                return __virtualname__
            else:
                return (False, "Cannot load rh_service module on SUSE > 11")

        osrelease_major = __grains__.get("osrelease_info", [0])[0]

        if __grains__["os"] in ("XenServer", "XCP-ng"):
            if osrelease_major >= 7:
                return (
                    False,
                    "XenServer and XCP-ng >= 7 use systemd, will not load rh_service.py"
                    " as virtual 'service'",
                )
            return __virtualname__

        if __grains__["os"] == "Fedora":
            if osrelease_major >= 15:
                return (
                    False,
                    "Fedora >= 15 uses systemd, will not load rh_service.py "
                    "as virtual 'service'",
                )
        if __grains__["os"] in (
            "RedHat",
            "CentOS",
            "ScientificLinux",
            "OEL",
            "CloudLinux",
        ):
            if osrelease_major >= 7:
                return (
                    False,
                    "RedHat-based distros >= version 7 use systemd, will not "
                    "load rh_service.py as virtual 'service'",
                )
        return __virtualname__
    return (False, f"Cannot load rh_service module: OS not in {enable}")


def _runlevel():
    """
    Return the current runlevel
    """
    out = __salt__["cmd.run"]("/sbin/runlevel")
    # unknown will be returned while inside a kickstart environment, since
    # this is usually a server deployment it should be safe to assume runlevel
    # 3.  If not all service related states will throw an out of range
    # exception here which will cause other functions to fail.
    if "unknown" in out:
        return "3"
    else:
        return out.split()[1]


def _chkconfig_add(name):
    """
    Run 'chkconfig --add' for a service whose script is installed in
    /etc/init.d.  The service is initially configured to be disabled at all
    run-levels.
    """
    cmd = f"/sbin/chkconfig --add {name}"
    if __salt__["cmd.retcode"](cmd, python_shell=False) == 0:
        log.info('Added initscript "%s" to chkconfig', name)
        return True
    else:
        log.error('Unable to add initscript "%s" to chkconfig', name)
        return False


def _service_is_upstart(name):
    """
    Return True if the service is an upstart service, otherwise return False.
    """
    return HAS_UPSTART and os.path.exists(f"/etc/init/{name}.conf")


def _service_is_sysv(name):
    """
    Return True if the service is a System V service (includes those managed by
    chkconfig); otherwise return False.
    """
    try:
        # Look for user-execute bit in file mode.
        return bool(os.stat(os.path.join("/etc/init.d", name)).st_mode & stat.S_IXUSR)
    except OSError:
        return False


def _service_is_chkconfig(name):
    """
    Return True if the service is managed by chkconfig.
    """
    cmdline = f"/sbin/chkconfig --list {name}"
    return (
        __salt__["cmd.retcode"](cmdline, python_shell=False, ignore_retcode=True) == 0
    )


def _sysv_is_enabled(name, runlevel=None):
    """
    Return True if the sysv (or chkconfig) service is enabled for the specified
    runlevel; otherwise return False.  If `runlevel` is None, then use the
    current runlevel.
    """
    # Try chkconfig first.
    result = _chkconfig_is_enabled(name, runlevel)
    if result:
        return True

    if runlevel is None:
        runlevel = _runlevel()
    return len(glob.glob(f"/etc/rc.d/rc{runlevel}.d/S??{name}")) > 0


def _chkconfig_is_enabled(name, runlevel=None):
    """
    Return ``True`` if the service is enabled according to chkconfig; otherwise
    return ``False``.  If ``runlevel`` is ``None``, then use the current
    runlevel.
    """
    cmdline = f"/sbin/chkconfig --list {name}"
    result = __salt__["cmd.run_all"](cmdline, python_shell=False)

    if runlevel is None:
        runlevel = _runlevel()
    if result["retcode"] == 0:
        for row in result["stdout"].splitlines():
            if f"{runlevel}:on" in row:
                if row.split()[0] == name:
                    return True
            elif row.split() == [name, "on"]:
                return True
    return False


def _sysv_enable(name):
    """
    Enable the named sysv service to start at boot.  The service will be enabled
    using chkconfig with default run-levels if the service is chkconfig
    compatible.  If chkconfig is not available, then this will fail.
    """
    if not _service_is_chkconfig(name) and not _chkconfig_add(name):
        return False
    cmd = f"/sbin/chkconfig {name} on"
    return not __salt__["cmd.retcode"](cmd, python_shell=False)


def _sysv_disable(name):
    """
    Disable the named sysv service from starting at boot.  The service will be
    disabled using chkconfig with default run-levels if the service is chkconfig
    compatible; otherwise, the service will be disabled for the current
    run-level only.
    """
    if not _service_is_chkconfig(name) and not _chkconfig_add(name):
        return False
    cmd = f"/sbin/chkconfig {name} off"
    return not __salt__["cmd.retcode"](cmd, python_shell=False)


def _sysv_delete(name):
    """
    Delete the named sysv service from the system. The service will be
    deleted using chkconfig.
    """
    if not _service_is_chkconfig(name):
        return False
    cmd = f"/sbin/chkconfig --del {name}"
    return not __salt__["cmd.retcode"](cmd)


def _upstart_delete(name):
    """
    Delete an upstart service. This will only rename the .conf file
    """
    if HAS_UPSTART:
        if os.path.exists(f"/etc/init/{name}.conf"):
            os.rename(
                f"/etc/init/{name}.conf",
                f"/etc/init/{name}.conf.removed",
            )
    return True


def _upstart_services():
    """
    Return list of upstart services.
    """
    if HAS_UPSTART:
        return [os.path.basename(name)[:-5] for name in glob.glob("/etc/init/*.conf")]
    else:
        return []


def _sysv_services():
    """
    Return list of sysv services.
    """
    _services = []
    output = __salt__["cmd.run"](["chkconfig", "--list"], python_shell=False)
    for line in output.splitlines():
        comps = line.split()
        try:
            if comps[1].startswith("0:"):
                _services.append(comps[0])
        except IndexError:
            continue
    # Return only the services that have an initscript present
    return [x for x in _services if _service_is_sysv(x)]


def get_enabled(limit=""):
    """
    Return the enabled services. Use the ``limit`` param to restrict results
    to services of that type.

    CLI Examples:

    .. code-block:: bash

        salt '*' service.get_enabled
        salt '*' service.get_enabled limit=upstart
        salt '*' service.get_enabled limit=sysvinit
    """
    limit = limit.lower()
    if limit == "upstart":
        return sorted(name for name in _upstart_services() if _upstart_is_enabled(name))
    elif limit == "sysvinit":
        runlevel = _runlevel()
        return sorted(
            name for name in _sysv_services() if _sysv_is_enabled(name, runlevel)
        )
    else:
        runlevel = _runlevel()
        return sorted(
            [name for name in _upstart_services() if _upstart_is_enabled(name)]
            + [name for name in _sysv_services() if _sysv_is_enabled(name, runlevel)]
        )


def get_disabled(limit=""):
    """
    Return the disabled services. Use the ``limit`` param to restrict results
    to services of that type.

    CLI Example:

    .. code-block:: bash

        salt '*' service.get_disabled
        salt '*' service.get_disabled limit=upstart
        salt '*' service.get_disabled limit=sysvinit
    """
    limit = limit.lower()
    if limit == "upstart":
        return sorted(
            name for name in _upstart_services() if not _upstart_is_enabled(name)
        )
    elif limit == "sysvinit":
        runlevel = _runlevel()
        return sorted(
            name for name in _sysv_services() if not _sysv_is_enabled(name, runlevel)
        )
    else:
        runlevel = _runlevel()
        return sorted(
            [name for name in _upstart_services() if not _upstart_is_enabled(name)]
            + [
                name
                for name in _sysv_services()
                if not _sysv_is_enabled(name, runlevel)
            ]
        )


def get_all(limit=""):
    """
    Return all installed services. Use the ``limit`` param to restrict results
    to services of that type.

    CLI Example:

    .. code-block:: bash

        salt '*' service.get_all
        salt '*' service.get_all limit=upstart
        salt '*' service.get_all limit=sysvinit
    """
    limit = limit.lower()
    if limit == "upstart":
        return sorted(_upstart_services())
    elif limit == "sysvinit":
        return sorted(_sysv_services())
    else:
        return sorted(_sysv_services() + _upstart_services())


def available(name, limit=""):
    """
    Return True if the named service is available.  Use the ``limit`` param to
    restrict results to services of that type.

    CLI Examples:

    .. code-block:: bash

        salt '*' service.available sshd
        salt '*' service.available sshd limit=upstart
        salt '*' service.available sshd limit=sysvinit
    """
    if limit == "upstart":
        return _service_is_upstart(name)
    elif limit == "sysvinit":
        return _service_is_sysv(name)
    else:
        return (
            _service_is_upstart(name)
            or _service_is_sysv(name)
            or _service_is_chkconfig(name)
        )


def missing(name, limit=""):
    """
    The inverse of service.available.
    Return True if the named service is not available.  Use the ``limit`` param to
    restrict results to services of that type.

    CLI Examples:

    .. code-block:: bash

        salt '*' service.missing sshd
        salt '*' service.missing sshd limit=upstart
        salt '*' service.missing sshd limit=sysvinit
    """
    if limit == "upstart":
        return not _service_is_upstart(name)
    elif limit == "sysvinit":
        return not _service_is_sysv(name)
    else:
        if _service_is_upstart(name) or _service_is_sysv(name):
            return False
        else:
            return True


def start(name):
    """
    Start the specified service

    CLI Example:

    .. code-block:: bash

        salt '*' service.start <service name>
    """
    if _service_is_upstart(name):
        cmd = f"start {name}"
    else:
        cmd = f"/sbin/service {name} start"
    return not __salt__["cmd.retcode"](cmd, python_shell=False)


def stop(name):
    """
    Stop the specified service

    CLI Example:

    .. code-block:: bash

        salt '*' service.stop <service name>
    """
    if _service_is_upstart(name):
        cmd = f"stop {name}"
    else:
        cmd = f"/sbin/service {name} stop"
    return not __salt__["cmd.retcode"](cmd, python_shell=False)


def restart(name):
    """
    Restart the named service

    CLI Example:

    .. code-block:: bash

        salt '*' service.restart <service name>
    """
    if _service_is_upstart(name):
        cmd = f"restart {name}"
    else:
        cmd = f"/sbin/service {name} restart"
    return not __salt__["cmd.retcode"](cmd, python_shell=False)


def reload_(name):
    """
    Reload the named service

    CLI Example:

    .. code-block:: bash

        salt '*' service.reload <service name>
    """
    if _service_is_upstart(name):
        cmd = f"reload {name}"
    else:
        cmd = f"/sbin/service {name} reload"
    return not __salt__["cmd.retcode"](cmd, python_shell=False)


def status(name, sig=None):
    """
    Return the status for a service.
    If the name contains globbing, a dict mapping service name to True/False
    values is returned.

    .. versionchanged:: 2018.3.0
        The service name can now be a glob (e.g. ``salt*``)

    Args:
        name (str): The name of the service to check
        sig (str): Signature to use to find the service via ps

    Returns:
        bool: True if running, False otherwise
        dict: Maps service name to True if running, False otherwise

    CLI Example:

    .. code-block:: bash

        salt '*' service.status <service name> [service signature]
    """
    if sig:
        return bool(__salt__["status.pid"](sig))

    contains_globbing = bool(re.search(r"\*|\?|\[.+\]", name))
    if contains_globbing:
        services = fnmatch.filter(get_all(), name)
    else:
        services = [name]
    results = {}
    for service in services:
        if _service_is_upstart(service):
            cmd = f"status {service}"
            results[service] = "start/running" in __salt__["cmd.run"](
                cmd, python_shell=False
            )
        else:
            cmd = f"/sbin/service {service} status"
            results[service] = (
                __salt__["cmd.retcode"](cmd, python_shell=False, ignore_retcode=True)
                == 0
            )
    if contains_globbing:
        return results
    return results[name]


def delete(name, **kwargs):
    """
    Delete the named service

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' service.delete <service name>
    """
    if _service_is_upstart(name):
        return _upstart_delete(name)
    else:
        return _sysv_delete(name)


def enable(name, **kwargs):
    """
    Enable the named service to start at boot

    CLI Example:

    .. code-block:: bash

        salt '*' service.enable <service name>
    """
    if _service_is_upstart(name):
        return _upstart_enable(name)
    else:
        return _sysv_enable(name)


def disable(name, **kwargs):
    """
    Disable the named service to start at boot

    CLI Example:

    .. code-block:: bash

        salt '*' service.disable <service name>
    """
    if _service_is_upstart(name):
        return _upstart_disable(name)
    else:
        return _sysv_disable(name)


def enabled(name, **kwargs):
    """
    Check to see if the named service is enabled to start on boot

    CLI Example:

    .. code-block:: bash

        salt '*' service.enabled <service name>
    """
    if _service_is_upstart(name):
        return _upstart_is_enabled(name)
    else:
        return _sysv_is_enabled(name)


def disabled(name):
    """
    Check to see if the named service is disabled to start on boot

    CLI Example:

    .. code-block:: bash

        salt '*' service.disabled <service name>
    """
    if _service_is_upstart(name):
        return not _upstart_is_enabled(name)
    else:
        return not _sysv_is_enabled(name)
