# -*- coding: utf-8 -*-

#########################################################################
# pyeole.service - manage EOLE services
# Copyright © 2012-2016 Pôle de Compétence EOLE <eole@ac-dijon.fr>
#
# License CeCILL:
#  * in french: http://www.cecill.info/licences/Licence_CeCILL_V2-fr.html
#  * in english http://www.cecill.info/licences/Licence_CeCILL_V2-en.html
#########################################################################

"""EOLE start/stop/restart/reload service or install/remove init script

Start, stop or restart available service
----------------------------------------

Generic interface to start, stop, restart or reload services in Creole
environment.

This implementation support two types:

* system V style init script
* upstart

All other service methods are ignored.

Examples:

    >>> from pyeole.log import init_logging
    >>> log = init_logging(level='info')
    >>> from pyeole.service import manage_services
    >>> try:
    ...     manage_services('start', 'servicename')
    ... except UnknownServiceError, err:
    ...     log.error(err)

Add service in boot sequence
----------------------------

There are several init script types. This implementation support
different's types:

* system V style init script
* upstart
* special services (like apache, ...)

Examples:

    >>> from pyeole.service import manage_services
    >>> manage_services('enable', 'servicename')
    >>> manage_services('disable', 'unwantedservice')

"""

import re

from os import symlink

from os.path import dirname
from os.path import join
from os.path import isfile
from os.path import relpath

from copy import copy
from glob import glob

from importlib import import_module

from creole.client import CreoleClient
from creole.client import NotFoundError
from creole.client import CreoleClientError

from pyeole.decorator import deprecated

# Base exception
from pyeole.service.error import ServiceError
# Configuration
from pyeole.service.error import DisabledError
from pyeole.service.error import ConfigureError
from pyeole.service.error import UnknownServiceError

from pyeole.service.launcher import get_commands

import logging

log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())

_ENCODING = 'utf-8'
"""Force encoding to UTF-8

"""

_CLIENT = CreoleClient()


SERVICES_LIST = None


def get_services_list():
    global SERVICES_LIST
    if SERVICES_LIST is None:
        #do something like {id_service: {key: values}}
        tmp_services = {}
        for key, value in list(_CLIENT.get('/containers/services').items()):
            srv, target = key.split('.')
            tmp_services.setdefault(int(srv[7:]), {})[target] = value

        #sort services by id and make a dict with all services with all containers in it
        id_services = list(tmp_services.keys())
        id_services.sort()
        services_order = []
        #services structure:
        #{'enable': set([u'root']),
        # u'name': u'networking',
        # u'level': u'module',
        # 'disable': set([]),
        # u'method': u'network',
        # u'pty': True}
        services = {}
        for id_srv in id_services:
            srv = tmp_services[id_srv]
            srv_name = srv['name']
            try:
                activate = srv.pop('activate')
            except KeyError:
                activate = False
            real_container = srv.pop('real_container')
            if srv_name not in services_order:
                services_order.append(srv_name)
                srv.pop('container')
                srv.pop('container_group')
                services[srv_name] = srv
                services[srv_name]['enable'] = set()
                services[srv_name]['disable'] = set()

            #is enable one time, always enable
            if activate:
                services[srv_name]['enable'].add(real_container)
                try:
                    services[srv_name]['disable'].remove(real_container)
                except KeyError:
                    pass
            else:
                if real_container not in services[srv_name]['enable']:
                    services[srv_name]['disable'].add(real_container)
        #make a list with all informations
        SERVICES_LIST = []
        for srv in services_order:
            SERVICES_LIST.append(services[srv])

    return SERVICES_LIST


def _services_is_active(container, srv):
    return (container is None and srv['enable'] != []) or (container is not None and container in srv['enable'])


def manage_services(action, names=None, container=None, display="log", exclude=None, force=False, containers_ctx=None):
    """Apply :data:`action` to one or several services.

    If no service :data:`names` is provided, :data:`action` is
    performed for all services.

    if no :data:`container` is provided, services are looked-up in all
    container groups.

    :param action: action to perform
    :type action: `str` in [``configure``, ``apply``, ``enable``,
                  ``disable``, ``status``, ``start``, ``restart``,
                  ``stop``, ``reload``]
    :param names: names of services
    :type names: `list` of `str`
    :param container: name of the container where to perform :data:`action`
    :type container: `str`
    :param display: type of message display "log", "console", "both"
    :type display: `str`
    :param exclude: exclude services
    :type exclude: `list`
    :param force: don't validate Creole state of service
    :type force: `bool`

    """
    if not force and action in ['enable', 'disable']:
        raise ConfigureError('cannot enable, disable directly use configure instead')

    if containers_ctx is None:
        containers_ctx = []
        if container is not None:
            containers_ctx = [_CLIENT.get_container_infos(container)]
            #container is the group name
            container = containers_ctx[0]['real_container']
        else:
            for group_name in _CLIENT.get_groups():
                containers_ctx.append(_CLIENT.get_group_infos(group_name))

    if names is not None and not isinstance(names, list):
        names = [names]

    services_list = get_services_list()
    # Check if service is unknown or disabled in all containers
    # construct services_str and services variables
    if names in [None, []]:
        services_str = u'all services'
        services = copy(services_list)
    else:
        services_str = u', '.join(names)
        services = []
        for name in names:
            found = False
            for srv in services_list:
                if srv['name'] == name:
                    found = True
                    if _services_is_active(container, srv):
                        services.append(srv)
                    else:
                        if not force or action in ['start', 'restart', 'reload']:
                            msg = 'cannot start disabled service {0}'.format(name)
                            raise DisabledError(msg)

            if not found:
                msg = u'Service unknown to creoled: {0}'.format(name)
                raise UnknownServiceError(msg)
    log.debug(u'Run {0} for {1}'.format(action, services_str))
    commands = get_commands(display)
    err_found = False
    if action == u'stop':
        services.reverse()

    # {'root': {'systemd': {'enabled': ['rng-tools', 'clamav-freshclam', 'clamav-daemon'],
    #                       'disabled': ['nut-server', 'apache2', 'eole-sso']}}
    # }
    multi_services = {}
    contexts = {}
    for service in services:
        command = commands[service['method']]
        for ctx in containers_ctx:
            cont = ctx['real_container']
            contexts[cont] = ctx
            multi_services.setdefault(cont, {})
            in_this_container = cont in service['enable'] or cont in service['disable']
            if in_this_container and (exclude is None or (ctx[u'name'] , service[u'name']) not in exclude):
                try:
                    # Skip uninstalled disabled service (#16134)
                    if cont in service['disable']:
                        _manage_service(u'check',
                                        service=service,
                                        ctx=ctx,
                                        command=command)
                except UnknownServiceError as err:
                    msg = u'Skip uninstalled and disabled service {0} in {1}'
                    log.debug(msg.format(service[u'name'], cont))
                    continue

                laction = action
                launch = True

                if action in ['start', 'restart', 'reload'] and cont not in service['enable']:
                    launch = False

                if action == 'configure':
                    if cont in service['enable']:
                        laction = 'enable'
                    elif cont in service['disable']:
                        laction = 'disable'
                    else:
                        launch = False

                if action == 'apply':
                    if cont in service['enable']:
                        laction = 'start'
                    elif cont in service['disable']:
                        laction = 'stop'
                    else:
                        launch = False

                # on ne traite pas les services ne supportant pas cette action
                if not 'cmd_' + laction + '_service' in dir(command):
                    launch = False

                if launch:
                    if command.multiple and len(services) != 1:
                        multi_services[cont].setdefault(service['method'], {}).setdefault(laction, []).append(service)
                    else:
                        ret = _manage_service(laction,
                                              service=service,
                                              ctx=ctx,
                                              command=command)
                        # on regarde si _manage_service a renvoyé un code de retour
                        if ret and ret.get('code', 0) != 0:
                            err_found = True
    for cont, multiservices in multi_services.items():
        ctx = contexts[cont]
        for command in ['restartonly', u'systemd', u'service', u'upstart', u'apache']:
            if command in multiservices:
                multi_service = multiservices[command]
                for action, services in multi_service.items():
                    ret = _manage_service(action,
                                          service=services,
                                          ctx=ctx,
                                          command=commands[command])
                    # on regarde si _manage_service a renvoyé un code de retour
                    if ret and ret.get('code', 0) != 0:
                        err_found = True

    return int(err_found)


def unmanaged_service(action, name, method, container='root', display='log', force=False, ctx=None):
    """Apply :data:`action` to a service not managed in creole.

    Prepare the service and context dictionaries to call
    :func:`_manage_service`.

    :param action: action to perform
    :type action: `str` in [``start``, ``restart``, ``stop``]
    :param name: name of service
    :type name: `str`
    :param method: method to manage service in [``service``, ``upstart``]
    :type metho: `str`
    :param container: name of the container where to perform :data:`action`
    :type container: `str`
    :param display: type of message display "log", "console", "both"
    :type display: `str`
    :param force: allow disable and enable action
    :type force: `boolean`

    """
    if method not in [u'systemd', u'service', u'upstart', u'apache', 'restartonly']:
        msg = u'Unauthorized service method for unmanaged service: {0}'
        raise ServiceError(msg.format(method))

    if action not in [u'start', u'restart', u'stop', u'status']:
        if force and action in [u'disable', u'enable']:
            pass
        else:
            msg = u'Unauthorized action for unmanaged service: {0}'
            raise ServiceError(msg.format(action))

    service = {u'name': name,
               u'method': method,
               u'activate': True,
               u'container': container,
               u'pty': True}

    if ctx is None:
        if container == 'root':
            ctx = {u'name': container,
                   u'ip': u'127.0.0.1',
                   u'path': '/'}
        else:
            try:
                ctx = _CLIENT.get_container(container)
            except CreoleClientError as err:
                # Avoid error if creoled is not running
                # but only for root container
                raise err


    commands = get_commands(display)
    # unmanaged_services: raises an exception if underlying code returns {'code':0, 'msg':err_msg}
    ret = _manage_service(action, service, ctx, command=commands[method])
    if ret and ret.get('code', 0) != 0:
        raise ServiceError(ret['msg'])


#####
##### Global worker
#####


def _manage_service(action, service, ctx, command):
    """Apply :data:`action` to :data:`services` in a container.

    :param action: action to perform
    :type action: `str` in [``configure``, ``apply``, ``enable``,
                  ``disable``, ``status``, ``start``, ``restart``,
                  ``stop``, ``reload``]
    :param services: names of service
    :type services: `list` of `str`
    :param ctx: container context
    :type ctx: `dict`
    :raise ServiceError: if the action is not valid
    :raise UnknownServiceError: if the service is unknown to creoled

    """
    #if action in ['start', 'restart', 'reload', 'configure']:
    #    command.check_service(service, ctx)
    return getattr(command, action + '_service')(service, ctx)


##########
########## OLD COLD FROM HERE
##########

##########
########## Deprecated API
##########

@deprecated(u'Use new API “manage_services()”')
def service_all(action, network=False, lxc=True):
    """Old API to run :data:`action` on all services

    Use the new :func:`manage_services` API.

    """
    manage_services(action)


@deprecated(u'Use new API “manage_services()”')
def service_code(service, action, container=u'root'):
    """Old API to run :data:`action` on a service in a container

    Use the new :func:`manage_services` API.

    """
    manage_services(action, service, container=container)


@deprecated(u'Use new API “manage_services()”')
def creole_service_code(service, action, container):
    """Old API to run :data:`action` on a service in a container

    Use the new :func:`manage_services` API.

    """
    manage_services(action, service[u'name'], container=container[u'name'])


def service_out(service, action, container=u'root'):
    """Old broken API to run :data:`action` on a service in a container

    Use the new :func:`manage_services` API.

    """
    msg = u'This code was broken, use “manage_services()” API'
    raise NotImplementedError(msg)


@deprecated(u'Use new API “manage_services()”')
def creole_service_out(service, action, container):
    """Old broken API to run :data:`action` on a service in a container

    Use the new :func:`manage_services` API.

    """
    manage_services(action, service[u'name'], container=container[u'name'])
    # Fake return value, manage_services raise if something goes wrong
    return 0, u'', u''


@deprecated(u'Use new API “manage_services()”')
def update_rcd(service, action, container=u'root'):
    """Old API to configure service in boot sequence.

    Use the new :func:`manage_services` API.

    """
    manage_services(action, service, container=container)


@deprecated(u'Use new API “manage_services()”')
def creole_update_rcd(service, action, container):
    """Old API to configure service in boot sequence.

    Use the new :func:`manage_services` API.

    """
    manage_services(action, service[u'name'], container=container[u'name'])


@deprecated(u'Use new API “manage_services()”')
def update_rcd_all():
    """Old API to configure services in boot sequence.

    Use the new :func:`manage_services` API.

    """
    manage_services(u'configure')


@deprecated(u'Use new API "manage_services()"')
def manage_service(action, name=None, container=None, display='log'):
    """Old API to start services.

    Use the new :func:`manage_services` API.

    """
    manage_services(action, names=name, container=container, display=display)
