import os
from base64 import b32encode, b32decode
from collections import OrderedDict
from six import BytesIO
from six.moves.urllib.parse import quote

from django.views.generic import FormView, ListView, TemplateView
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import load_backend
from django.contrib.auth.views import LoginView
from django.contrib.auth.decorators import login_required
from django.contrib import auth, messages
from django.conf import settings
from django.http import HttpResponseRedirect
from django.urls import reverse, reverse_lazy
from django.utils.http import is_safe_url, urlencode
from django.shortcuts import resolve_url, get_object_or_404
from django.contrib.sites.shortcuts import get_current_site
from django.utils.functional import cached_property
from django.utils.translation import ugettext as _

import qrcode
from qrcode.image.svg import SvgPathImage
from u2flib_server import u2f

from .forms import KeyResponseForm, BackupCodeForm, TOTPForm, KeyRegistrationForm
from .models import TOTPDevice


class U2FLoginView(LoginView):
    form_class = AuthenticationForm
    template_name = 'u2f/login.html'

    @property
    def is_admin(self):
        return self.template_name == 'admin/login.html'

    def requires_two_factor(self, user):
        return (user.u2f_keys.exists() or
                user.backup_codes.exists() or
                user.totp_devices.exists())

    def form_valid(self, form):
        user = form.get_user()
        if not self.requires_two_factor(user):
            # no keys registered, use single-factor auth
            return super(U2FLoginView, self).form_valid(form)
        else:
            self.request.session['u2f_pre_verify_user_pk'] = user.pk
            self.request.session['u2f_pre_verify_user_backend'] = user.backend

            verify_url = reverse('u2f:verify-second-factor')
            redirect_to = self.request.POST.get(auth.REDIRECT_FIELD_NAME,
                                                self.request.GET.get(auth.REDIRECT_FIELD_NAME, ''))
            params = {}
            if is_safe_url(url=redirect_to, allowed_hosts=self.request.get_host()):
                params[auth.REDIRECT_FIELD_NAME] = redirect_to
            if self.is_admin:
                params['admin'] = 1
            if params:
                verify_url += '?' + urlencode(params)

            return HttpResponseRedirect(verify_url)

    def get_context_data(self, **kwargs):
        kwargs = super(U2FLoginView, self).get_context_data(**kwargs)
        kwargs[auth.REDIRECT_FIELD_NAME] = self.request.GET.get(auth.REDIRECT_FIELD_NAME, '')
        kwargs.update(self.kwargs.get('extra_context', {}))
        return kwargs


class AdminU2FLoginView(U2FLoginView):
    template_name = 'admin/login.html'


class OriginMixin(object):
    def get_origin(self):
        return '{scheme}://{host}'.format(
            scheme=self.request.scheme,
            host=self.request.get_host(),
        )


class AddKeyView(OriginMixin, FormView):
    template_name = 'u2f/add_key.html'
    form_class = KeyRegistrationForm
    success_url = reverse_lazy('u2f:u2f-keys')

    def dispatch(self, request, *args, **kwargs):
        return super(AddKeyView, self).dispatch(request, *args, **kwargs)

    def get_form_kwargs(self):
        kwargs = super(AddKeyView, self).get_form_kwargs()
        kwargs.update(
            user=self.request.user,
            request=self.request,
            appId=self.get_origin(),
        )
        return kwargs

    def get_context_data(self, **kwargs):
        kwargs = super(AddKeyView, self).get_context_data(**kwargs)
        request = u2f.begin_registration(self.get_origin(), [
            key.to_json() for key in self.request.user.u2f_keys.all()
        ])
        self.request.session['u2f_registration_request'] = request
        kwargs['registration_request'] = request

        return kwargs

    def form_valid(self, form):
        response = form.cleaned_data['response']
        request = self.request.session['u2f_registration_request']
        del self.request.session['u2f_registration_request']
        device, attestation_cert = u2f.complete_registration(request, response)
        self.request.user.u2f_keys.create(
            public_key=device['publicKey'],
            key_handle=device['keyHandle'],
            app_id=device['appId'],
        )
        messages.success(self.request, _("Key added."))
        return super(AddKeyView, self).form_valid(form)

    def get_success_url(self):
        if 'next' in self.request.GET and is_safe_url(self.request.GET['next']):
            return self.request.GET['next']
        else:
            return super(AddKeyView, self).get_success_url()


class VerifySecondFactorView(OriginMixin, TemplateView):
    template_name = 'u2f/verify_second_factor.html'

    @property
    def form_classes(self):
        ret = {}
        if self.user.u2f_keys.exists():
            ret['u2f'] = KeyResponseForm
        if self.user.backup_codes.exists():
            ret['backup'] = BackupCodeForm
        if self.user.totp_devices.exists():
            ret['totp'] = TOTPForm

        return ret

    def get_user(self):
        try:
            user_id = self.request.session['u2f_pre_verify_user_pk']
            backend_path = self.request.session['u2f_pre_verify_user_backend']
            assert backend_path in settings.AUTHENTICATION_BACKENDS
            backend = load_backend(backend_path)
            user = backend.get_user(user_id)
            if user is not None:
                user.backend = backend_path
            return user
        except (KeyError, AssertionError):
            return None

    def dispatch(self, request, *args, **kwargs):
        self.user = self.get_user()
        if self.user is None:
            return HttpResponseRedirect(reverse('u2f:login'))
        return super(VerifySecondFactorView, self).dispatch(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        forms = self.get_forms()
        form = forms[request.POST['type']]
        if form.is_valid():
            return self.form_valid(form, forms)
        else:
            return self.form_invalid(forms)

    def form_invalid(self, forms):
        return self.render_to_response(self.get_context_data(
            forms=forms,
        ))

    def get_form_kwargs(self):
        return {
            'user': self.user,
            'request': self.request,
            'appId': self.get_origin(),
        }

    def get_forms(self):
        kwargs = self.get_form_kwargs()
        if self.request.method == 'GET':
            forms = {key: form(**kwargs) for key, form in self.form_classes.items()}
        else:
            method = self.request.POST['type']
            forms = {
                key: form(**kwargs)
                for key, form in self.form_classes.items()
                if key != method
            }
            forms[method] = self.form_classes[method](self.request.POST, **kwargs)
        return forms

    def get_context_data(self, **kwargs):
        if 'forms' not in kwargs:
            kwargs['forms'] = self.get_forms()
        kwargs = super(VerifySecondFactorView, self).get_context_data(**kwargs)
        if self.request.GET.get('admin'):
            kwargs['base_template'] = 'admin/base_site.html'
        else:
            kwargs['base_template'] = 'base.html'
        kwargs['user'] = self.user
        return kwargs

    def form_valid(self, form, forms):
        if not form.validate_second_factor():
            return self.form_invalid(forms)

        del self.request.session['u2f_pre_verify_user_pk']
        del self.request.session['u2f_pre_verify_user_backend']

        auth.login(self.request, self.user)

        redirect_to = self.request.POST.get(auth.REDIRECT_FIELD_NAME,
                                            self.request.GET.get(auth.REDIRECT_FIELD_NAME, ''))
        if not is_safe_url(url=redirect_to, allowed_hosts=self.request.get_host()):
            redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)
        return HttpResponseRedirect(redirect_to)


class TwoFactorSettingsView(TemplateView):
    template_name = 'u2f/two_factor_settings.html'

    def get_context_data(self, **kwargs):
        context= super(TwoFactorSettingsView, self).get_context_data(**kwargs)
        context['u2f_enabled'] = self.request.user.u2f_keys.exists()
        context['backup_codes'] = self.request.user.backup_codes.all()
        context['totp_enabled'] = self.request.user.totp_devices.exists()
        return context

class KeyManagementView(ListView):
    template_name = 'u2f/key_list.html'

    def get_queryset(self):
        return self.request.user.u2f_keys.all()

    def post(self, request):
        assert 'delete' in self.request.POST
        key = get_object_or_404(self.get_queryset(), pk=self.request.POST['key_id'])
        key.delete()
        if self.get_queryset().exists():
            messages.success(request, _("Key removed."))
        else:
            messages.success(request, _("Key removed. Two-factor auth disabled."))
        return HttpResponseRedirect(reverse('u2f:u2f-keys'))


class BackupCodesView(ListView):
    template_name = 'u2f/backup_codes.html'

    def get_queryset(self):
        return self.request.user.backup_codes.all()

    def post(self, request):
        for i in range(10):
            self.request.user.backup_codes.create_backup_code()
        return HttpResponseRedirect(self.request.build_absolute_uri())


class AddTOTPDeviceView(OriginMixin, FormView):
    form_class = TOTPForm
    template_name = 'u2f/totp_device.html'
    success_url = reverse_lazy('u2f:two-factor-settings')

    def gen_key(self):
        return os.urandom(20)

    def get_otpauth_url(self, key):
        secret = b32encode(key)
        issuer = get_current_site(self.request).name

        params = OrderedDict([
            ('secret', secret),
            ('digits', 6),
            ('issuer', issuer),
        ])

        return 'otpauth://totp/{issuer}:{username}?{params}'.format(
            issuer=quote(issuer),
            username=quote(self.request.user.get_username()),
            params=urlencode(params),
        )

    def get_qrcode(self, data):
        img = qrcode.make(data, image_factory=SvgPathImage)
        buf = BytesIO()
        img.save(buf)
        return buf.getvalue()

    @cached_property
    def key(self):
        try:
            return b32decode(self.request.POST['base32_key'])
        except KeyError:
            return self.gen_key()

    def get_context_data(self, **kwargs):
        kwargs = super(AddTOTPDeviceView, self).get_context_data(**kwargs)
        kwargs['base32_key'] = b32encode(self.key).decode()
        kwargs['otpauth'] = self.get_otpauth_url(self.key) 
        kwargs['qr_svg'] = self.get_qrcode(kwargs['otpauth'])
        return kwargs

    def get_form_kwargs(self):
        kwargs = super(AddTOTPDeviceView, self).get_form_kwargs()
        kwargs.update(
            user=self.request.user,
            request=self.request,
            appId=self.get_origin(),
        )
        return kwargs

    def form_valid(self, form):
        device = TOTPDevice(
            user=self.request.user,
            key=self.key,
        )
        if device.validate_token(form.cleaned_data['token']):
            device.save()
            messages.success(self.request, _("Device added."))
            return super(AddTOTPDeviceView, self).form_valid(form)
        else:
            assert not device.pk
            form.add_error('token', TOTPForm.INVALID_ERROR_MESSAGE)
            return self.form_invalid(form)

    def form_invalid(self, form):
        # Should this go in Django's FormView?!
        # <https://code.djangoproject.com/ticket/25548>
        return self.render_to_response(self.get_context_data(form=form))

    def get_success_url(self):
        if 'next' in self.request.GET and is_safe_url(self.request.GET['next']):
            return self.request.GET['next']
        else:
            return super(AddTOTPDeviceView, self).get_success_url()


class TOTPDeviceManagementView(ListView):
    template_name = 'u2f/totpdevice_list.html'

    def get_queryset(self):
        return self.request.user.totp_devices.all()

    def post(self, request):
        assert 'delete' in self.request.POST
        device = get_object_or_404(self.get_queryset(), pk=self.request.POST['device_id'])
        device.delete()
        messages.success(request, _("Device removed."))
        return HttpResponseRedirect(reverse('u2f:totp-devices'))


add_key = login_required(AddKeyView.as_view())
verify_second_factor = VerifySecondFactorView.as_view()
login = U2FLoginView.as_view()
keys = login_required(KeyManagementView.as_view())
two_factor_settings = login_required(TwoFactorSettingsView.as_view())
backup_codes = login_required(BackupCodesView.as_view())
add_totp = login_required(AddTOTPDeviceView.as_view())
totp_devices = login_required(TOTPDeviceManagementView.as_view())