# -*- coding: utf-8 -*-
#
# Copyright 2013 - Mirantis, Inc.
#
#    Licensed under the Apache License, Version 2.0 (the "License");
#    you may not use this file except in compliance with the License.
#    You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS,
#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#    See the License for the specific language governing permissions and
#    limitations under the License.

import eventlet
from oslo import messaging
from pecan import hooks

from mistral import exceptions as exc
from mistral.openstack.common import jsonutils
from mistral.openstack.common import log as logging
from mistral import utils

LOG = logging.getLogger(__name__)

# TODO(rakhmerov): refactor all threadlocal things with mistral.utils

_CTX_THREAD_LOCAL_NAME = "MISTRAL_APP_CTX_THREAD_LOCAL"


class BaseContext(object):
    """Container for context variables."""

    _elements = set()

    def __init__(self, __mapping=None, **kwargs):
        if __mapping is None:
            self.__values = dict(**kwargs)
        else:
            if isinstance(__mapping, BaseContext):
                __mapping = __mapping.__values
            self.__values = dict(__mapping)
            self.__values.update(**kwargs)

        bad_keys = set(self.__values) - self._elements

        if bad_keys:
            raise TypeError("Only %s keys are supported. %s given" %
                            (tuple(self._elements), tuple(bad_keys)))

    def __getattr__(self, name):
        try:
            return self.__values[name]
        except KeyError:
            if name in self._elements:
                return None
            else:
                raise AttributeError(name)

    def to_dict(self):
        return self.__values


class MistralContext(BaseContext):
    _elements = set([
        "user_id",
        "project_id",
        "auth_token",
        "service_catalog",
        "user_name",
        "project_name",
        "roles",
        "is_admin",
    ])


def has_ctx():
    return utils.has_thread_local(_CTX_THREAD_LOCAL_NAME)


def ctx():
    if not has_ctx():
        raise exc.ApplicationContextNotFoundException()

    return utils.get_thread_local(_CTX_THREAD_LOCAL_NAME)


def set_ctx(new_ctx):
    utils.set_thread_local(_CTX_THREAD_LOCAL_NAME, new_ctx)


def _wrapper(context, thread_desc, thread_group, func, *args, **kwargs):
    try:
        set_ctx(context)
        func(*args, **kwargs)
    except Exception as e:
        LOG.exception("Thread '%s' fails with exception: '%s'"
                      % (thread_desc, e))
        if thread_group and not thread_group.exc:
            thread_group.exc = e
            thread_group.failed_thread = thread_desc
    finally:
        if thread_group:
            thread_group._on_thread_exit()

        set_ctx(None)


def spawn(thread_description, func, *args, **kwargs):
    eventlet.spawn(_wrapper, ctx().clone(), thread_description,
                   None, func, *args, **kwargs)


def context_from_headers(headers):
    return MistralContext(
        user_id=headers.get('X-User-Id'),
        project_id=headers.get('X-Project-Id'),
        auth_token=headers.get('X-Auth-Token'),
        service_catalog=headers.get('X-Service-Catalog'),
        user_name=headers.get('X-User-Name'),
        project_name=headers.get('X-Project-Name'),
        roles=headers.get('X-Roles', "").split(",")
    )


class JsonPayloadSerializer(messaging.NoOpSerializer):
    @staticmethod
    def serialize_entity(context, entity):
        return jsonutils.to_primitive(entity, convert_instances=True)


class RpcContextSerializer(messaging.Serializer):

    def __init__(self, base=None):
        self._base = base or messaging.NoOpSerializer()

    def serialize_entity(self, context, entity):
        if not self._base:
            return entity
        return self._base.serialize_entity(context, entity)

    def deserialize_entity(self, context, entity):
        if not self._base:
            return entity
        return self._base.deserialize_entity(context, entity)

    def serialize_context(self, context):
        return context.to_dict()

    def deserialize_context(self, context):
        ctx = MistralContext(**context)
        set_ctx(ctx)
        return ctx


class ContextHook(hooks.PecanHook):
    def before(self, state):
        request_ctx = context_from_headers(state.request.headers)
        set_ctx(request_ctx)

    def after(self, state):
        set_ctx(None)
