from importlib import import_module

from django.contrib.auth import authenticate, login as django_login
from django.contrib.auth.models import User, AnonymousUser
from django.contrib.sessions.models import Session
from django.forms.models import model_to_dict
from django.conf import settings
from django.http import HttpRequest
from django.apps import apps

from .. import RpcInvalidParamsError
from ..rpc import JsonRpcMethod
from . import AuthBackend


class DjangoAuthBackend(AuthBackend):
    def __init__(self, generic_orm_methods=False):
        self.generic_orm_methods = generic_orm_methods
        self.session_engine = import_module(settings.SESSION_ENGINE)

    # Helper methods
    def get_user(self, request):
        session_key = request.cookies.get('sessionid', '')

        if session_key:
            try:
                session = Session.objects.get(session_key=session_key)
                uid = session.get_decoded().get('_auth_user_id')

                try:
                    return User.objects.get(pk=uid)

                except User.DoesNotExist:
                    pass

            except Session.DoesNotExist:
                pass

        return AnonymousUser()

    def _is_authorized(self, request, method):
        if hasattr(method, 'login_required') and (
           not request.user.is_active or
           not request.user.is_authenticated()):
            return False

        # permission check
        if(hasattr(method, 'permissions_required') and
           not request.user.is_superuser and
           not request.user.has_perms(method.permissions_required)):
            return False

        # user tests
        if hasattr(method, 'tests') and not request.user.is_superuser:
            for test in method.tests:
                if not test(request.user):
                    return False

        return True

    # generic ORM methods
    def dump_model_object(self, obj):
        d = model_to_dict(obj)
        d['pk'] = obj.pk

        return d

    async def _model_view(self, request, model):
        lookups = request.msg.data['params'] or {}

        if not isinstance(lookups, dict):
            raise RpcInvalidParamsError

        try:
            objects = model.objects.filter(**lookups)

            return [self.dump_model_object(i) for i in objects]

        except Exception:
            raise RpcInvalidParamsError

    async def _model_delete(self, request, model):
        lookups = request.msg.data['params'] or {}

        if not isinstance(lookups, dict):
            raise RpcInvalidParamsError

        try:
            model.objects.filter(**lookups).delete()

        except Exception:
            raise RpcInvalidParamsError

        return True

    async def _model_add(self, request, model):
        values = request.msg.data['params'] or {}

        if not isinstance(values, dict) or not values:
            raise RpcInvalidParamsError

        try:
            new_object = model.objects.create(**values)

            return self.dump_model_object(new_object)

        except Exception:
            raise RpcInvalidParamsError

    async def _model_change(self, request, model):
        try:
            params = request.msg.data['params']
            pk = params.pop('pk')

            model_object = model.objects.get(pk=pk)

            for field_name, value in params.items():
                setattr(model_object, field_name, value)

            model_object.save()

            return True

        except KeyError:
            raise RpcInvalidParamsError

    async def handle_orm_call(self, request):
        method_name = request.msg.data['method'].split('__')[1]
        app_label, _ = method_name.split('.')
        action, model_name = _.split('_')
        model = apps.get_model('{}.{}'.format(app_label, model_name))

        if action == 'view':
            return await self._model_view(request, model)

        elif action == 'add':
            return await self._model_add(request, model)

        elif action == 'change':
            return await self._model_change(request, model)

        elif action == 'delete':
            return await self._model_delete(request, model)

    # login / logout
    async def login(self, request):
        try:
            username = str(request.params['username'])
            password = str(request.params['password'])

        except(KeyError, TypeError, ValueError):
            raise RpcInvalidParamsError

        user = authenticate(username=username, password=password)

        if not user:
            return False

        # to use the standard django login mechanism, which is build on the
        # request-, response-system, we have to fake a django http request
        fake_request = HttpRequest()
        fake_request.session = self.session_engine.SessionStore()
        django_login(fake_request, user)
        fake_request.session.save()

        # set session cookie
        request.http_request.ws.set_cookie(
            name=settings.SESSION_COOKIE_NAME,
            value=fake_request.session.session_key,
            path='/',
            max_age=None,
            domain=settings.SESSION_COOKIE_DOMAIN,
            secure=settings.SESSION_COOKIE_SECURE or None,
            expires=None,
        )

        # rediscover methods and topics
        self.prepare_request(request.http_request, user=user)

        return True

    # request processing
    def prepare_request(self, request, user=None):
        request.user = user or self.get_user(request)
        request.methods = {}

        # django auth methods
        if isinstance(request.user, AnonymousUser):
            request.methods['login'] = JsonRpcMethod(self.login)

        # generic django model methods
        if self.generic_orm_methods:
            for permission_name in request.user.get_all_permissions():
                action = permission_name.split('.')[1].split('_')[0]
                method_name = 'db__{}'.format(permission_name)

                if action in ('view', 'add', 'change', 'delete', ):
                    request.methods[method_name] = JsonRpcMethod(
                        self.handle_orm_call)

        # rpc defined methods
        for name, method in request.rpc.methods.items():
            if self._is_authorized(request, method.method):
                request.methods[name] = method

        # topics
        request.topics = set()

        for name, method in request.rpc.topics.items():
            if self._is_authorized(request, method):
                request.topics.add(name)

        if not hasattr(request, 'subscriptions'):
            request.subscriptions = set()

        request.subscriptions = request.topics & request.subscriptions