from functools import lru_cache

from django.conf import settings
from django.contrib.auth import logout as auth_logout
from django.db.utils import IntegrityError
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.request import Request

from authentication import errors
from authentication.mixins import AuthMixin
from authentication.notifications import OAuthBindMessage
from authentication.decorators import post_save_next_to_session_if_guard_redirect
from common.utils import get_logger
from common.utils.common import get_request_ip
from common.utils.django import reverse, get_object_or_none
from users.models import User
from users.signal_handlers import bind_user_to_org_role, check_only_allow_exist_user_auth
from .mixins import FlashMessageMixin

logger = get_logger(__file__)


class IMClientMixin:
    client_type_path = ''
    client_auth_params = {}

    @property
    @lru_cache(maxsize=1)
    def client(self):
        if not all([self.client_type_path, self.client_auth_params]):
            raise NotImplementedError
        client_init = {k: getattr(settings, v) for k, v in self.client_auth_params.items()}
        client_type = import_string(self.client_type_path)
        return client_type(**client_init)


class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, IMClientMixin, View):
    user_type = ''
    auth_backend = None
    # 提示信息
    msg_client_err = _('Error')
    msg_user_not_bound_err = _('Error')
    msg_not_found_user_from_client_err = _('Error')

    def verify_state(self):
        raise NotImplementedError

    def create_user_if_not_exist(self, user_id, **kwargs):
        user = None
        user_attr = self.client.get_user_detail(user_id, **kwargs)
        try:
            exists = User.objects.filter(username=user_attr['username']).exists()
            if not check_only_allow_exist_user_auth(not exists):
                return None, (self.msg_client_err, self.request.error_message)

            user, create = User.objects.get_or_create(
                username=user_attr['username'], defaults=user_attr
            )

            setattr(user, f'{self.user_type}_id', user_id)
            if create:
                setattr(user, 'source', self.user_type)
                bind_user_to_org_role(user)
            user.save()
        except IntegrityError as err:
            logger.error(f'{self.msg_client_err}: create user error: {err}')

        if user is None:
            title = self.msg_client_err
            msg = _('If you have any question, please contact the administrator')
            return user, (title, msg)

        return user, None

    @post_save_next_to_session_if_guard_redirect
    def get(self, request: Request):
        code = request.GET.get('code')
        redirect_url = request.GET.get('redirect_url')
        login_url = reverse('authentication:login')

        if not self.verify_state():
            return self.get_verify_state_failed_response(redirect_url)

        try:
            user_id, other_info = self.client.get_user_id_by_code(code)
        except Exception:
            response = self.get_failed_response(
                login_url, title=self.msg_client_err,
                msg=self.msg_not_found_user_from_client_err
            )
            return response

        if not user_id:
            # 正常流程不会出这个错误，hack 行为
            err = self.msg_not_found_user_from_client_err
            response = self.get_failed_response(login_url, title=err, msg=err)
            return response

        user = get_object_or_none(User, **{f'{self.user_type}_id': user_id})
        if user is None:
            user, err = self.create_user_if_not_exist(user_id, other_info=other_info)
            if err is not None:
                response = self.get_failed_response(login_url, title=err[0], msg=err[1])
                return response

        try:
            self.check_oauth2_auth(user, getattr(settings, self.auth_backend))
        except errors.AuthFailedError as e:
            self.set_login_failed_mark()
            msg = e.msg
            response = self.get_failed_response(login_url, title=msg, msg=msg)
            return response

        return self.redirect_to_guard_view()


class BaseBindCallbackView(FlashMessageMixin, IMClientMixin, View):
    auth_type = ''
    auth_type_label = ''

    def verify_state(self):
        raise NotImplementedError

    def get_already_bound_response(self, redirect_uri):
        raise NotImplementedError

    def get(self, request: Request):
        code = request.GET.get('code')
        redirect_url = request.GET.get('redirect_url')

        if not self.verify_state():
            return self.get_verify_state_failed_response(redirect_url)

        user = request.user
        source_id = getattr(user, f'{self.auth_type}_id', None)
        if source_id:
            response = self.get_already_bound_response(redirect_url)
            return response

        auth_user_id, __ = self.client.get_user_id_by_code(code)
        if not auth_user_id:
            msg = _('%s query user failed') % self.auth_type_label
            response = self.get_failed_response(redirect_url, msg, msg)
            return response

        try:
            setattr(user, f'{self.auth_type}_id', auth_user_id)
            user.save()
        except IntegrityError as e:
            msg = _('The %s is already bound to another user') % self.auth_type_label
            logger.error(e, exc_info=True)
            response = self.get_failed_response(redirect_url, msg, msg)
            return response

        ip = get_request_ip(request)
        OAuthBindMessage(user, ip, self.auth_type_label, auth_user_id).publish_async()
        msg = _('Binding %s successfully') % self.auth_type_label
        auth_logout(request)
        response = self.get_success_response(redirect_url, msg, msg)
        return response
