# Copyright (c) 2012-2016 Seafile Ltd.
import logging
from binascii import unhexlify
from base64 import b32encode

from constance import config
from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site
from django.core.urlresolvers import reverse
from django.forms import Form
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.shortcuts import redirect
from django.utils.http import is_safe_url
from django.utils.module_loading import import_string
from django.views.decorators.cache import never_cache
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, DeleteView, TemplateView
from django.views.generic.base import View

import qrcode
import qrcode.image.svg

try:
    from formtools.wizard.views import SessionWizardView
except ImportError:
    # pylint: disable=import-error,no-name-in-module
    from django.contrib.formtools.wizard.views import SessionWizardView

from seahub.auth import login as login, REDIRECT_FIELD_NAME
from seahub.auth.decorators import login_required
from seahub.auth.forms import AuthenticationForm

from seahub.two_factor import login as two_factor_login
from seahub.two_factor.decorators import otp_required
from seahub.two_factor.models import (StaticToken, StaticDevice,
                                            PhoneDevice, get_available_methods)
from seahub.two_factor.utils import random_hex, totp_digits

from seahub.two_factor.forms import (MethodForm, TOTPDeviceForm,
                                           PhoneNumberForm, DeviceValidationForm)
from seahub.two_factor.utils import get_otpauth_url, default_device
from seahub.two_factor.views.utils import (class_view_decorator,
                                                 CheckTwoFactorEnabledMixin,
                                                 IdempotentSessionWizardView)

logger = logging.getLogger(__name__)

QR_SESSION_KEY = 'django_two_factor-qr_secret_key'

@class_view_decorator(never_cache)
@class_view_decorator(login_required)
class SetupView(CheckTwoFactorEnabledMixin, IdempotentSessionWizardView):

    redirect_url = 'two_factor:backup_tokens'
    qrcode_url = 'two_factor:qr'
    template_name = 'two_factor/core/setup.html'
    initial_dict = {}
    form_list = (
        # ('welcome', Form),
        ('method', MethodForm),
        ('generator', TOTPDeviceForm),
        ('sms', PhoneNumberForm),
        ('call', PhoneNumberForm),
        ('validation', DeviceValidationForm),
    )
    condition_dict = {
        'generator': lambda self: self.get_method() == 'generator',
        'call': lambda self: self.get_method() == 'call',
        'sms': lambda self: self.get_method() == 'sms',
        'validation': lambda self: self.get_method() in ('sms', 'call'),
    }

    def get_method(self):
        method_data = self.storage.validated_step_data.get('method', {})
        return method_data.get('method', None)

    def get(self, request, *args, **kwargs):
        """
        Start the setup wizard. Redirect if already enabled.
        """
        if default_device(self.request.user):
            return redirect(self.redirect_url)
        return super(SetupView, self).get(request, *args, **kwargs)

    def get_form_list(self):
        """
        Check if there is only one method, then skip the MethodForm from form_list
        """
        form_list = super(SetupView, self).get_form_list()
        available_methods = get_available_methods()
        if len(available_methods) == 1:
            form_list.pop('method', None)

            # XXX: since we comment out first welcome step, `form_list` will
            # be empty after pop 'method', which will cause index error in
            # `WizardView::get` when reset to the first step, so we have to
            # add our default method to the form list.
            if len(form_list) == 0:
                form_list['generator'] = TOTPDeviceForm

            method_key, _ = available_methods[0]
            self.storage.validated_step_data['method'] = {'method': method_key}
        return form_list

    def render_next_step(self, form, **kwargs):
        """
        In the validation step, ask the device to generate a challenge.
        """
        next_step = self.steps.next
        if next_step == 'validation':
            try:
                self.get_device().generate_challenge()
                kwargs["challenge_succeeded"] = True
            except:
                logger.exception("Could not generate challenge")
                kwargs["challenge_succeeded"] = False
        return super(SetupView, self).render_next_step(form, **kwargs)

    def done(self, form_list, **kwargs):
        """
        Finish the wizard. Save all forms and redirect.
        """
        # TOTPDeviceForm
        if self.get_method() == 'generator':
            form = [form for form in form_list if isinstance(form, TOTPDeviceForm)][0]
            device = form.save()

        # PhoneNumberForm / YubiKeyDeviceForm
        elif self.get_method() in ('call', 'sms', 'yubikey'):
            device = self.get_device()
            device.save()

        else:
            raise NotImplementedError("Unknown method '%s'" % self.get_method())

        two_factor_login(self.request, device)

        device = StaticDevice.get_or_create(self.request.user.username)
        if device.token_set.count() == 0:
            device.generate_tokens()

        return redirect(self.redirect_url)

    def get_form_kwargs(self, step=None):
        kwargs = {}
        if step == 'generator':
            kwargs.update({
                'key': self.get_key(step),
                'user': self.request.user,
            })
        if step in ('validation', 'yubikey'):
            kwargs.update({
                'device': self.get_device()
            })
        metadata = self.get_form_metadata(step)
        if metadata:
            kwargs.update({'metadata': metadata, })
        return kwargs

    def get_device(self, **kwargs):
        """
        Uses the data from the setup step and generated key to recreate device.

        Only used for call / sms -- generator uses other procedure.
        """
        method = self.get_method()
        kwargs = kwargs or {}
        kwargs['name'] = 'default'
        kwargs['user'] = self.request.user

        if method in ('call', 'sms'):
            kwargs['method'] = method
            kwargs['number'] = self.storage.validated_step_data\
                .get(method, {}).get('number')
            return PhoneDevice(key=self.get_key(method), **kwargs)

    def get_key(self, step):
        self.storage.extra_data.setdefault('keys', {})
        if step in self.storage.extra_data['keys']:
            return self.storage.extra_data['keys'].get(step)
        key = random_hex(20).decode('ascii')
        self.storage.extra_data['keys'][step] = key
        return key

    def get_context_data(self, form, **kwargs):
        context = super(SetupView, self).get_context_data(form, **kwargs)
        if self.steps.current == 'generator':
            key = self.get_key('generator')
            rawkey = unhexlify(key.encode('ascii'))
            b32key = b32encode(rawkey).decode('utf-8')
            self.request.session[QR_SESSION_KEY] = b32key
            context.update({'QR_URL': reverse(self.qrcode_url)})
        elif self.steps.current == 'validation':
            context['device'] = self.get_device()
        context['cancel_url'] = reverse('edit_profile')
        return context

    def process_step(self, form):
        if hasattr(form, 'metadata'):
            self.storage.extra_data.setdefault('forms', {})
            self.storage.extra_data['forms'][
                self.steps.current] = form.metadata
        return super(SetupView, self).process_step(form)

    def get_form_metadata(self, step):
        self.storage.extra_data.setdefault('forms', {})
        return self.storage.extra_data['forms'].get(step, None)


@class_view_decorator(never_cache)
@class_view_decorator(otp_required)
class BackupTokensView(CheckTwoFactorEnabledMixin, FormView):
    """
    View for listing and generating backup tokens.

    A user can generate a number of static backup tokens. When the user loses
    its phone, these backup tokens can be used for verification. These backup
    tokens should be stored in a safe location; either in a safe or underneath
    a pillow ;-).
    """
    form_class = Form
    redirect_url = 'two_factor:backup_tokens'
    template_name = 'two_factor/core/backup_tokens.html'
    number_of_tokens = 10

    def get_device(self):
        return StaticDevice.get_or_create(self.request.user.username)

    def get_context_data(self, **kwargs):
        context = super(BackupTokensView, self).get_context_data(**kwargs)
        context['device'] = self.get_device()
        return context

    def form_valid(self, form):
        """
        Delete existing backup codes and generate new ones.
        """
        device = self.get_device()
        device.token_set.all().delete()
        device.generate_tokens()

        return redirect(self.redirect_url)


@class_view_decorator(never_cache)
@class_view_decorator(otp_required)
class SetupCompleteView(CheckTwoFactorEnabledMixin, TemplateView):
    """
    View congratulation the user when OTP setup has completed.
    """
    template_name = 'two_factor/core/setup_complete.html'

    def get_context_data(self):
        return {'phone_methods': [], }


@class_view_decorator(never_cache)
@class_view_decorator(login_required)
class QRGeneratorView(View):
    """
    View returns an SVG image with the OTP token information
    """
    http_method_names = ['get']
    default_qr_factory = 'qrcode.image.svg.SvgPathImage'

    # The qrcode library only supports PNG and SVG for now
    image_content_types = {
        'PNG': 'image/png',
        'SVG': 'image/svg+xml; charset=utf-8',
    }

    def get(self, request, *args, **kwargs): # pylint: disable=unused-argument
        # Get the data from the session
        if not config.ENABLE_TWO_FACTOR_AUTH:
            raise Http404()
        try:
            key = self.request.session[QR_SESSION_KEY]
            del self.request.session[QR_SESSION_KEY]
        except KeyError:
            raise Http404()

        # Get data for qrcode
        image_factory_string = getattr(settings, 'TWO_FACTOR_QR_FACTORY',
                                       self.default_qr_factory)
        image_factory = import_string(image_factory_string)
        content_type = self.image_content_types[image_factory.kind]

        otpauth_url = get_otpauth_url(
            accountname=self.request.user.username,
            issuer=get_current_site(self.request).name,
            secret=key,
            digits=totp_digits())

        # Make and return QR code
        img = qrcode.make(otpauth_url, image_factory=image_factory)
        resp = HttpResponse(content_type=content_type)
        img.save(resp)
        return resp
