# -*- coding: utf-8 -*-
#
# Copyright (C) 2015-2016 Avencall
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>

from builtins import object
import logging
import signal
import sys

from functools import partial

from cheroot.wsgi import PathInfoDispatcher
from cheroot.wsgi import Server
from celery import Celery
from consul import Consul
from flask import Flask
from flask_restful import Api
from flask_cors import CORS
from stevedore.dispatch import NameDispatchExtensionManager
from threading import Thread
from xivo import http_helpers
from xivo.consul_helpers import ServiceCatalogRegistration

from xivo_auth import http, token, extensions, static_token_manager

from .service_discovery import self_check

logger = logging.getLogger(__name__)


def _signal_handler(signum, frame):
    sys.exit(0)


class Controller(object):

    def __init__(self, config):
        self._config = config
        try:
            self._listen_addr = config['rest_api']['https']['listen']
            self._listen_port = config['rest_api']['https']['port']
            self._foreground = config['foreground']
            self._cors_config = config['rest_api']['cors']
            self._cors_enabled = self._cors_config['enabled']
            self._consul_config = config['consul']
            self._service_discovery_config = config['service_discovery']
            self._plugins = config['enabled_plugins']
            self._bus_uri = config['amqp']['uri']
            self._bus_config = config['amqp']
            self._log_level = config['log_level']
            self._debug = config['debug']
            self._bind_addr = (self._listen_addr, self._listen_port)
            self._ssl_cert_file = config['rest_api']['https']['certificate']
            self._ssl_key_file = config['rest_api']['https']['private_key']
            self._ssl_ciphers = config['rest_api']['https']['ciphers']
            self._xivo_uuid = config.get('uuid')
            logger.debug('private key: %s', self._ssl_key_file)
        except KeyError as e:
            logger.error('Missing configuration to start the application: %s', e)
            sys.exit(1)

        backends = self._load_backends()
        self._config['loaded_plugins'] = self._loaded_plugins_names(backends)

        self._celery = self._configure_celery()
        consul_client = Consul(**self._consul_config)
        token_storage = token.Storage(consul_client)
        token_manager = token.Manager(config, token_storage, self._celery, self._getStaticTokens())
        self._flask_app = self._configure_flask_app(backends, token_manager)
        self._override_celery_task()

    def run(self):
        self._start_celery_worker()
        signal.signal(signal.SIGTERM, _signal_handler)
        wsgi_app = PathInfoDispatcher({'/': self._flask_app})
        server = Server(bind_addr=self._bind_addr, wsgi_app=wsgi_app)
        server.ssl_adapter = http_helpers.ssl_adapter(self._ssl_cert_file,
                                                      self._ssl_key_file,
                                                      self._ssl_ciphers)

        with ServiceCatalogRegistration('xivo-auth',
                                        self._xivo_uuid,
                                        self._consul_config,
                                        self._service_discovery_config,
                                        self._bus_config,
                                        partial(self_check, self._listen_port)):
            try:
                server.start()
            finally:
                server.stop()

    def _start_celery_worker(self):
        args = sys.argv[:1] + ['-P', 'threads']
        celery_thread = Thread(target=self._celery.start, args=(args,))
        celery_thread.daemon = True
        celery_thread.start()

    def _load_backends(self):
        return _PluginLoader(self._config).load()

    def _loaded_plugins_names(self, backends):
        return [backend.name for backend in backends]

    def _configure_celery(self):
        celery = Celery('xivo-auth', broker=self._bus_uri)
        celery.conf.update(self._config)
        celery.conf.update(
            CELERY_RESULT_BACKEND=self._bus_uri,
            CELERY_ACCEPT_CONTENT=['json'],
            CELERY_TASK_SERIALIZER='json',
            CELERY_RESULT_SERIALIZER='json',
            CELERY_IGNORE_RESULT=True,
            CELERY_ALWAYS_EAGER=False,
            CELERY_EAGER_PROPAGATES_EXCEPTIONS=True,
            CELERY_DEFAULT_EXCHANGE_TYPE='topic',
            CELERYD_LOG_LEVEL='debug' if self._debug else self._log_level,
            CELERYD_HIJACK_ROOT_LOGGER=False,
            BROKER_TRANSPORT_OPTIONS={'max_retries': 2},
        )
        return celery

    def _override_celery_task(self):
        TaskBase = self._celery.Task
        app = self._flask_app

        class ContextTask(TaskBase):
            abstract = True

            def __call__(self, *args, **kwargs):
                with app.app_context():
                    return TaskBase.__call__(self, app, *args, **kwargs)

        self._celery.Task = ContextTask
        extensions.celery = self._celery
        from xivo_auth import tasks  # noqa

    def _configure_flask_app(self, backends, token_manager):
        app = Flask('xivo-auth')
        http_helpers.add_logger(app, logger)
        api = Api(app, prefix='/0.1')
        api.add_resource(http.Tokens, '/token')
        api.add_resource(http.Token, '/token/<string:token>')
        api.add_resource(http.Backends, '/backends')
        api.add_resource(http.Api, '/api/api.json')
        app.config.update(self._config)
        if self._cors_enabled:
            CORS(app, **self._cors_config)

        app.config['token_manager'] = token_manager
        app.config['backends'] = backends
        app.after_request(http_helpers.log_request)

        return app

    def _getStaticTokens(self):
        try:
            xc = self._config['static_service_tokens']['xc']
            return dict([(xc['token'],xc['acl'])])
        except:
            logger.warning("Unable to load static tokens, continuing without")
        return dict()

class _PluginLoader(object):

    namespace = 'xivo_auth.backends'

    def __init__(self, config):
        self._enabled_plugins = config['enabled_plugins']
        self._config = config
        self._backends = NameDispatchExtensionManager(namespace=self.namespace,
                                                      check_func=self._check,
                                                      verify_requirements=False,
                                                      propagate_map_exceptions=True,
                                                      invoke_on_load=False)

    def load(self):
        self._backends.map(self._enabled_plugins, self._load)
        return self._backends

    def _check(self, plugin):
        if plugin.name in self._enabled_plugins:
            if plugin.plugin.should_be_loaded(self._config):
                return True
            logger.info('Plugin %s is not configured', plugin.name)
        return False

    def _load(self, extension):
        try:
            extension.obj = extension.plugin(self._config)
        except Exception:
            logger.exception('Failed to load %s', extension.name)
