# -*- coding: utf-8 -*-
'''
The service module for FreeBSD

.. 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>`.
'''
from __future__ import absolute_import, unicode_literals, print_function

# Import python libs
import logging
import os
import fnmatch
import re

# Import salt libs
import salt.utils.path
import salt.utils.decorators as decorators
import salt.utils.files
from salt.exceptions import CommandNotFoundError

__func_alias__ = {
    'reload_': 'reload'
}

log = logging.getLogger(__name__)

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


def __virtual__():
    '''
    Only work on FreeBSD
    '''
    # Disable on these platforms, specific service modules exist:
    if __grains__['os'] == 'FreeBSD':
        return __virtualname__
    return (False, 'The freebsdservice execution module cannot be loaded: only available on FreeBSD systems.')


@decorators.memoize
def _cmd(jail=None):
    '''
    Return full path to service command

    .. versionchanged:: 2016.3.4

    Support for jail (representing jid or jail name) keyword argument in kwargs
    '''
    service = salt.utils.path.which('service')
    if not service:
        raise CommandNotFoundError('\'service\' command not found')
    if jail:
        jexec = salt.utils.path.which('jexec')
        if not jexec:
            raise CommandNotFoundError('\'jexec\' command not found')
        service = '{0} {1} {2}'.format(jexec, jail, service)
    return service


def _get_jail_path(jail):
    '''
    .. versionadded:: 2016.3.4

    Return the jail's root directory (path) as shown in jls

    jail
        The jid or jail name
    '''
    jls = salt.utils.path.which('jls')
    if not jls:
        raise CommandNotFoundError('\'jls\' command not found')
    jails = __salt__['cmd.run_stdout']('{0} -n jid name path'.format(jls))
    for j in jails.splitlines():
        jid, jname, path = (x.split('=')[1].strip() for x in j.split())
        if jid == jail or jname == jail:
            return path.rstrip('/')
    # XΧΧ, TODO, not sure how to handle nonexistent jail
    return ''


def _get_rcscript(name, jail=None):
    '''
    Return full path to service rc script

    .. versionchanged:: 2016.3.4

    Support for jail (representing jid or jail name) keyword argument in kwargs
    '''
    cmd = '{0} -r'.format(_cmd(jail))
    prf = _get_jail_path(jail) if jail else ''
    for line in __salt__['cmd.run_stdout'](cmd, python_shell=False).splitlines():
        if line.endswith('{0}{1}'.format(os.path.sep, name)):
            return os.path.join(prf, line.lstrip(os.path.sep))
    return None


def _get_rcvar(name, jail=None):
    '''
    Return rcvar

    .. versionchanged:: 2016.3.4

    Support for jail (representing jid or jail name) keyword argument in kwargs
    '''
    if not available(name, jail):
        log.error('Service %s not found', name)
        return False

    cmd = '{0} {1} rcvar'.format(_cmd(jail), name)

    for line in __salt__['cmd.run_stdout'](cmd, python_shell=False).splitlines():
        if '_enable="' not in line:
            continue
        rcvar, _ = line.split('=', 1)
        return rcvar

    return None


def get_enabled(jail=None):
    '''
    Return what services are set to run on boot

    .. versionchanged:: 2016.3.4

    Support for jail (representing jid or jail name) keyword argument in kwargs

    CLI Example:

    .. code-block:: bash

        salt '*' service.get_enabled
    '''
    ret = []
    service = _cmd(jail)
    prf = _get_jail_path(jail) if jail else ''
    for svc in __salt__['cmd.run']('{0} -e'.format(service)).splitlines():
        ret.append(os.path.basename(svc))

    # This is workaround for bin/173454 bug
    for svc in get_all(jail):
        if svc in ret:
            continue
        if not os.path.exists('{0}/etc/rc.conf.d/{1}'.format(prf, svc)):
            continue
        if enabled(svc, jail=jail):
            ret.append(svc)

    return sorted(ret)


def get_disabled(jail=None):
    '''
    Return what services are available but not enabled to start at boot

    .. versionchanged:: 2016.3.4

    Support for jail (representing jid or jail name) keyword argument in kwargs

    CLI Example:

    .. code-block:: bash

        salt '*' service.get_disabled
    '''
    en_ = get_enabled(jail)
    all_ = get_all(jail)
    return sorted(set(all_) - set(en_))


def _switch(name,                   # pylint: disable=C0103
            on,                     # pylint: disable=C0103
            **kwargs):
    '''
    Switch on/off service start at boot.

    .. versionchanged:: 2016.3.4

    Support for jail (representing jid or jail name) and chroot keyword argument
    in kwargs. chroot should be used when jail's /etc is mounted read-only and
    should point to a root directory where jail's /etc is mounted read-write.
    '''
    jail = kwargs.get('jail', '')
    chroot = kwargs.get('chroot', '').rstrip('/')
    if not available(name, jail):
        return False

    rcvar = _get_rcvar(name, jail)
    if not rcvar:
        log.error('rcvar for service %s not found', name)
        return False

    if jail and not chroot:
        # prepend the jail's path in config paths when referring to a jail, when
        # chroot is not provided. chroot should be provided when the jail's /etc
        # is mounted read-only
        chroot = _get_jail_path(jail)

    config = kwargs.get('config',
                        __salt__['config.option']('service.config',
                                                  default='{0}/etc/rc.conf'.format(chroot)
                                                  )
                        )

    if not config:
        rcdir = '{0}/etc/rc.conf.d'.format(chroot)
        if not os.path.exists(rcdir) or not os.path.isdir(rcdir):
            log.error('%s not exists', rcdir)
            return False
        config = os.path.join(rcdir, rcvar.replace('_enable', ''))

    nlines = []
    edited = False

    if on:
        val = 'YES'
    else:
        val = 'NO'

    if os.path.exists(config):
        with salt.utils.files.fopen(config, 'r') as ifile:
            for line in ifile:
                line = salt.utils.stringutils.to_unicode(line)
                if not line.startswith('{0}='.format(rcvar)):
                    nlines.append(line)
                    continue
                rest = line[len(line.split()[0]):]  # keep comments etc
                nlines.append('{0}="{1}"{2}'.format(rcvar, val, rest))
                edited = True
    if not edited:
        # Ensure that the file ends in a \n
        if len(nlines) > 1 and nlines[-1][-1] != '\n':
            nlines[-1] = '{0}\n'.format(nlines[-1])
        nlines.append('{0}="{1}"\n'.format(rcvar, val))

    with salt.utils.files.fopen(config, 'w') as ofile:
        nlines = [salt.utils.stringutils.to_str(_l) for _l in nlines]
        ofile.writelines(nlines)

    return True


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

    name
        service name

    config : /etc/rc.conf
        Config file for managing service. If config value is
        empty string, then /etc/rc.conf.d/<service> used.
        See man rc.conf(5) for details.

        Also service.config variable can be used to change default.

    .. versionchanged:: 2016.3.4

    jail (optional keyword argument)
        the jail's id or name

    chroot (optional keyword argument)
        the jail's chroot, if the jail's /etc is not mounted read-write

    CLI Example:

    .. code-block:: bash

        salt '*' service.enable <service name>
    '''
    return _switch(name, True, **kwargs)


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

    Arguments the same as for enable()

    .. versionchanged:: 2016.3.4

    jail (optional keyword argument)
        the jail's id or name

    chroot (optional keyword argument)
        the jail's chroot, if the jail's /etc is not mounted read-write

    CLI Example:

    .. code-block:: bash

        salt '*' service.disable <service name>
    '''
    return _switch(name, False, **kwargs)


def enabled(name, **kwargs):
    '''
    Return True if the named service is enabled, false otherwise

    name
        Service name

    .. versionchanged:: 2016.3.4

    Support for jail (representing jid or jail name) keyword argument in kwargs

    CLI Example:

    .. code-block:: bash

        salt '*' service.enabled <service name>
    '''
    jail = kwargs.get('jail', '')
    if not available(name, jail):
        log.error('Service %s not found', name)
        return False

    cmd = '{0} {1} rcvar'.format(_cmd(jail), name)

    for line in __salt__['cmd.run_stdout'](cmd, python_shell=False).splitlines():
        if '_enable="' not in line:
            continue
        _, state, _ = line.split('"', 2)
        return state.lower() in ('yes', 'true', 'on', '1')

    # probably will never reached
    return False


def disabled(name, **kwargs):
    '''
    Return True if the named service is enabled, false otherwise

    CLI Example:

    .. code-block:: bash

        salt '*' service.disabled <service name>
    '''
    return not enabled(name, **kwargs)


def available(name, jail=None):
    '''
    Check that the given service is available.

    .. versionchanged:: 2016.3.4

    jail: optional jid or jail name

    CLI Example:

    .. code-block:: bash

        salt '*' service.available sshd
    '''
    return name in get_all(jail)


def missing(name, jail=None):
    '''
    The inverse of service.available.
    Returns ``True`` if the specified service is not available, otherwise returns
    ``False``.

    .. versionchanged:: 2016.3.4

    jail: optional jid or jail name

    CLI Example:

    .. code-block:: bash

        salt '*' service.missing sshd
    '''
    return name not in get_all(jail)


def get_all(jail=None):
    '''
    Return a list of all available services

    .. versionchanged:: 2016.3.4

    jail: optional jid or jail name

    CLI Example:

    .. code-block:: bash

        salt '*' service.get_all
    '''
    ret = []
    service = _cmd(jail)
    for srv in __salt__['cmd.run']('{0} -l'.format(service)).splitlines():
        if not srv.isupper():
            ret.append(srv)
    return sorted(ret)


def start(name, jail=None):
    '''
    Start the specified service

    .. versionchanged:: 2016.3.4

    jail: optional jid or jail name

    CLI Example:

    .. code-block:: bash

        salt '*' service.start <service name>
    '''
    cmd = '{0} {1} onestart'.format(_cmd(jail), name)
    return not __salt__['cmd.retcode'](cmd, python_shell=False)


def stop(name, jail=None):
    '''
    Stop the specified service

    .. versionchanged:: 2016.3.4

    jail: optional jid or jail name

    CLI Example:

    .. code-block:: bash

        salt '*' service.stop <service name>
    '''
    cmd = '{0} {1} onestop'.format(_cmd(jail), name)
    return not __salt__['cmd.retcode'](cmd, python_shell=False)


def restart(name, jail=None):
    '''
    Restart the named service

    .. versionchanged:: 2016.3.4

    jail: optional jid or jail name

    CLI Example:

    .. code-block:: bash

        salt '*' service.restart <service name>
    '''
    cmd = '{0} {1} onerestart'.format(_cmd(jail), name)
    return not __salt__['cmd.retcode'](cmd, python_shell=False)


def reload_(name, jail=None):
    '''
    Restart the named service

    .. versionchanged:: 2016.3.4

    jail: optional jid or jail name

    CLI Example:

    .. code-block:: bash

        salt '*' service.reload <service name>
    '''
    cmd = '{0} {1} onereload'.format(_cmd(jail), name)
    return not __salt__['cmd.retcode'](cmd, python_shell=False)


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

    .. versionchanged:: 2016.3.4

    .. 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:
        cmd = '{0} {1} onestatus'.format(_cmd(jail), service)
        results[service] = not __salt__['cmd.retcode'](cmd,
                                        python_shell=False,
                                        ignore_retcode=True)
    if contains_globbing:
        return results
    return results[name]
