# This file is part of Indico.
# Copyright (C) 2002 - 2021 CERN
#
# Indico is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see the
# LICENSE file for more details.

from flask import flash, jsonify, redirect, render_template, request, session
from itsdangerous import BadData, BadSignature
from markupsafe import Markup
from webargs import fields
from werkzeug.exceptions import BadRequest, Forbidden, NotFound

from indico.core import signals
from indico.core.auth import login_rate_limiter, multipass
from indico.core.config import config
from indico.core.db import db
from indico.core.notifications import make_email, send_email
from indico.modules.admin import RHAdminBase
from indico.modules.auth import Identity, logger, login_user
from indico.modules.auth.forms import (AddLocalIdentityForm, EditLocalIdentityForm, LocalRegistrationForm,
                                       MultipassRegistrationForm, RegistrationEmailForm, ResetPasswordEmailForm,
                                       ResetPasswordForm, SelectEmailForm)
from indico.modules.auth.models.registration_requests import RegistrationRequest
from indico.modules.auth.util import impersonate_user, load_identity_info, register_user, undo_impersonate_user
from indico.modules.auth.views import WPAuth, WPAuthUser
from indico.modules.users import User
from indico.modules.users.controllers import RHUserBase
from indico.util.i18n import _
from indico.util.signing import secure_serializer
from indico.web.args import use_kwargs
from indico.web.flask.templating import get_template_module
from indico.web.flask.util import url_for
from indico.web.forms.base import FormDefaults, IndicoForm
from indico.web.rh import RH
from indico.web.util import url_for_index


def _get_provider(name, external):
    try:
        provider = multipass.auth_providers[name]
    except KeyError:
        raise NotFound('Provider does not exist')
    if provider.is_external != external:
        raise NotFound('Invalid provider')
    return provider


class RHLogin(RH):
    """The login page."""

    # Disable global CSRF check. The form might not be an IndicoForm
    # but a normal WTForm from Flask-WTF which does not use the same
    # CSRF token as Indico forms use. But since the form has its own
    # CSRF check anyway disabling the global check is perfectly fine.
    CSRF_ENABLED = False

    def _process(self):
        login_reason = session.pop('login_reason', None)

        # User is already logged in
        if session.user is not None:
            multipass.set_next_url()
            return multipass.redirect_success()

        # Some clients attempt to incorrectly resolve redirections internally.
        # See https://github.com/indico/indico/issues/4720 for details
        user_agent = request.headers.get('User-Agent', '')
        sso_redirect = not any(s in user_agent for s in ('ms-office', 'Microsoft Office'))

        # If we have only one provider, and this provider is external, we go there immediately
        # However, after a failed login we need to show the page to avoid a redirect loop
        if not session.pop('_multipass_auth_failed', False) and 'provider' not in request.view_args and sso_redirect:
            single_auth_provider = multipass.single_auth_provider
            if single_auth_provider and single_auth_provider.is_external:
                multipass.set_next_url()
                return redirect(url_for('.login', provider=single_auth_provider.name))

        # Save the 'next' url to go to after login
        multipass.set_next_url()

        # If there's a provider in the URL we start the external login process
        if 'provider' in request.view_args:
            provider = _get_provider(request.view_args['provider'], True)
            return provider.initiate_external_login()

        # If we have a POST request we submitted a login form for a local provider
        rate_limit_exceeded = False
        if request.method == 'POST':
            active_provider = provider = _get_provider(request.form['_provider'], False)
            form = provider.login_form()
            rate_limit_exceeded = not login_rate_limiter.test()
            if not rate_limit_exceeded and form.validate_on_submit():
                response = multipass.handle_login_form(provider, form.data)
                if response:
                    return response
                # re-check since a failed login may have triggered the rate limit
                rate_limit_exceeded = not login_rate_limiter.test()
        # Otherwise we show the form for the default provider
        else:
            active_provider = multipass.default_local_auth_provider
            form = active_provider.login_form() if active_provider else None

        providers = list(multipass.auth_providers.values())
        retry_in = login_rate_limiter.get_reset_delay() if rate_limit_exceeded else None
        return render_template('auth/login_page.html', form=form, providers=providers, active_provider=active_provider,
                               login_reason=login_reason, retry_in=retry_in)


class RHLoginForm(RH):
    """Retrieve a login form (json)."""

    def _process(self):
        provider = _get_provider(request.view_args['provider'], False)
        form = provider.login_form()
        template_module = get_template_module('auth/_login_form.html')
        return jsonify(success=True, html=template_module.login_form(provider, form))


class RHLogout(RH):
    """Log the user out."""

    def _process(self):
        next_url = request.args.get('next')
        if not next_url or not multipass.validate_next_url(next_url):
            next_url = url_for_index()
        return multipass.logout(next_url, clear_session=True)


def _send_confirmation(email, salt, endpoint, template, template_args=None, url_args=None, data=None):
    template_args = template_args or {}
    url_args = url_args or {}
    token = secure_serializer.dumps(data or email, salt=salt)
    url = url_for(endpoint, token=token, _external=True, **url_args)
    template_module = get_template_module(template, email=email, url=url, **template_args)
    send_email(make_email(email, template=template_module))
    flash(_('We have sent you a verification email. Please check your mailbox within the next hour and open '
            'the link in that email.'))
    return redirect(url_for(endpoint, **url_args))


class RHLinkAccount(RH):
    """Link a new identity with an existing user.

    This RH is only used if the identity information contains an
    email address and an existing user was found.
    """

    def _process_args(self):
        self.identity_info = load_identity_info()
        if not self.identity_info or self.identity_info['indico_user_id'] is None:
            # Just redirect to the front page or whereever we wanted to go.
            # Probably someone simply used his browser's back button.
            flash('There is no pending login.', 'warning')
            return multipass.redirect_success()
        self.user = User.get(self.identity_info['indico_user_id'])
        self.emails = sorted(self.user.all_emails & set(self.identity_info['data'].getlist('email')))
        self.verification_email_sent = self.identity_info.get('verification_email_sent', False)
        self.email_verified = self.identity_info['email_verified']
        self.must_choose_email = len(self.emails) != 1 and not self.email_verified

    def _process(self):
        if self.verification_email_sent and 'token' in request.args:
            email = secure_serializer.loads(request.args['token'], max_age=3600, salt='link-identity-email')
            if email not in self.emails:
                raise BadData('Emails do not match')
            session['login_identity_info']['email_verified'] = True
            session.modified = True
            flash(_('You have successfully validated your email address and can now proceed with the login.'),
                  'success')
            return redirect(url_for('.link_account', provider=self.identity_info['provider']))

        if self.must_choose_email:
            form = SelectEmailForm()
            form.email.choices = list(zip(self.emails, self.emails))
        else:
            form = IndicoForm()

        if form.validate_on_submit():
            if self.email_verified:
                return self._create_identity()
            elif not self.verification_email_sent:
                return self._send_confirmation(form.email.data if self.must_choose_email else self.emails[0])
            else:
                flash(_('The validation email has already been sent.'), 'warning')

        return WPAuth.render_template('link_identity.html', identity_info=self.identity_info, user=self.user,
                                      email_sent=self.verification_email_sent, emails=' / '.join(self.emails),
                                      form=form, must_choose_email=self.must_choose_email)

    def _create_identity(self):
        identity = Identity(user=self.user, provider=self.identity_info['provider'],
                            identifier=self.identity_info['identifier'], data=self.identity_info['data'],
                            multipass_data=self.identity_info['multipass_data'])
        logger.info('Created new identity for %s: %s', self.user, identity)
        del session['login_identity_info']
        db.session.flush()
        login_user(self.user, identity)
        return multipass.redirect_success()

    def _send_confirmation(self, email):
        session['login_identity_info']['verification_email_sent'] = True
        session['login_identity_info']['data']['email'] = email  # throw away other emails
        return _send_confirmation(email, 'link-identity-email', '.link_account',
                                  'auth/emails/link_identity_verify_email.txt', {'user': self.user},
                                  url_args={'provider': self.identity_info['provider']})


class RHRegister(RH):