# -*- coding: utf-8 -*-
import furl
from rest_framework import status as http_status
from future.moves.urllib.parse import urlencode

import markupsafe
from django.core.exceptions import ValidationError
from django.utils import timezone
from flask import request

from addons.osfstorage.models import Region
from framework import forms, sentry, status
from framework import auth as framework_auth
from framework.auth import exceptions
from framework.auth import cas, campaigns
from framework.auth import logout as osf_logout
from framework.auth import get_user
from framework.auth.exceptions import DuplicateEmailError, ExpiredTokenError, InvalidTokenError
from framework.auth.core import generate_verification_key
from framework.auth.decorators import block_bing_preview, collect_auth, must_be_logged_in
from framework.auth.forms import ResendConfirmationForm, ForgotPasswordForm, ResetPasswordForm
from framework.auth.utils import ensure_external_identity_uniqueness, validate_recaptcha
from framework.exceptions import HTTPError
from framework.flask import redirect  # VOL-aware redirect
from framework.sessions.utils import remove_sessions_for_user, remove_session
from framework.sessions import get_session
from framework.utils import throttle_period_expired
from osf.models import OSFUser
from osf.utils.sanitize import strip_html
from website import settings, mails, language
from api.waffle.utils import storage_i18n_flag_active
from website.util import web_url_for
from osf.exceptions import ValidationValueError, BlockedEmailError
from osf.models.provider import PreprintProvider
from osf.models.tag import Tag
from osf.utils.requests import check_select_for_update
from website.util.metrics import CampaignClaimedTags, CampaignSourceTags
from website.ember_osf_web.decorators import ember_flag_is_active
from osf import features
#from osf.models import PreprintProvider


@block_bing_preview
@collect_auth
def reset_password_get(auth, uid=None, token=None):
    """Identical to ``reset_password_institution_get`` b/c ``website/routes.py`` framework requires
    unique view methods for routes using the same renderer/template."""
    return _reset_password_get(auth, uid=uid, token=token)


@block_bing_preview
@collect_auth
def reset_password_institution_get(auth, uid=None, token=None):
    """Identical to ``reset_password_get`` b/c ``website/routes.py`` framework requires unique view
    methods for routes using the same renderer/template."""
    return _reset_password_get(auth, uid=uid, token=token, institutional=True)


def _reset_password_get(auth, uid=None, token=None, institutional=False):
    """
    View for user to land on the reset password page.  Takes a unique token generated by the
    forgot-password page and (if valid) grants the user a new temporary token to allow them to
    reset their password.  User is redirected to the reset password page.  If token is not valid,
    returns an error page and 400 Bad Request status code.

    HTTP Method: GET

    :param auth: the authentication state
    :param uid: the user id
    :param token: the token in verification key
    :param institutional: is this the institutional reset password page?
    :return
    :raises: HTTPError(http_status.HTTP_400_BAD_REQUEST) if verification key for the user is invalid, has expired or was used
    """

    # if users are logged in, log them out and redirect back to this page
    if auth.logged_in:
        return auth_logout(redirect_url=request.url)

    # Check if request bears a valid pair of `uid` and `token`
    user_obj = OSFUser.load(uid)
    if not (user_obj and user_obj.verify_password_token(token=token)):
        error_data = {
            'message_short': 'Invalid Request.',
            'message_long': 'The requested URL is invalid, has expired, or was already used',
        }
        raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=error_data)

    # refresh the verification key (v2)
    user_obj.verification_key_v2 = generate_verification_key(verification_type='password')
    user_obj.save()

    # override routes.py login_url to redirect to dashboard
    service_url = web_url_for('dashboard', _absolute=True)

    return {
        'uid': user_obj._id,
        'token': user_obj.verification_key_v2['token'],
        'login_url': service_url,
        'isInstitutional': institutional,
        # view knows paths better than template
        'resetPath': 'resetpassword-institution' if institutional else 'resetpassword',
    }


def reset_password_post(uid=None, token=None):
    """
    View for user to submit reset password form.  Accepts a temporary password token.  If password
    token is valid, reset user's password to the value from the submitted form.

    HTTP Method: POST

    :param uid: the user id
    :param token: the token in verification key
    :return:
    :raises: HTTPError(http_status.HTTP_400_BAD_REQUEST) if verification key for the user is invalid, has expired or was used
    """

    form = ResetPasswordForm(request.form)

    # Check if request bears a valid pair of `uid` and `token`
    user_obj = OSFUser.load(uid)
    if not (user_obj and user_obj.verify_password_token(token=token)):
        error_data = {
            'message_short': 'Invalid Request.',
            'message_long': 'The requested URL is invalid, has expired, or was already used',
        }
        raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=error_data)

    if not form.validate():
        # Don't go anywhere
        forms.push_errors_to_status(form.errors)
    else:
        # clear verification key (v2)
        user_obj.verification_key_v2 = {}
        # new verification key (v1) for CAS
        user_obj.verification_key = generate_verification_key(verification_type=None)
        try:
            user_obj.set_password(form.password.data)
            osf4m_source_tag, created = Tag.all_tags.get_or_create(name=CampaignSourceTags.Osf4m.value, system=True)
            osf4m_claimed_tag, created = Tag.all_tags.get_or_create(name=CampaignClaimedTags.Osf4m.value, system=True)
            if user_obj.all_tags.filter(id=osf4m_source_tag.id, system=True).exists():
                user_obj.add_system_tag(osf4m_claimed_tag)
            user_obj.save()
        except exceptions.ChangePasswordError as error:
            for message in error.messages:
                status.push_status_message(message, kind='warning', trust=False)
        else:
            status.push_status_message('Password reset', kind='success', trust=False)
            # redirect to CAS and authenticate the user automatically with one-time verification key.
            return redirect(cas.get_login_url(
                web_url_for('user_account', _absolute=True),
                username=user_obj.username,
                verification_key=user_obj.verification_key
            ))

    return {
        'uid': user_obj._id,
        'token': user_obj.verification_key_v2['token'],
    }


def reset_password_institution_post(uid=None, token=None):
    """Behaves identically to ``reset_password_post``, so just dispatch to that method"""
    return reset_password_post(uid=uid, token=token)


@collect_auth
def forgot_password_get(auth):
    """
    View for user to land on the forgot password page.  Logs user out if they are already logged
    in.

    HTTP Method: GET

    :param auth: the authentication context
    :return
    """

    # if users are logged in, log them out and redirect back to this page
    if auth.logged_in:
        return auth_logout(redirect_url=request.url)

    #overriding the routes.py sign in url to redirect to the dashboard after login
    context = {}
    context['login_url'] = web_url_for('dashboard', _absolute=True)

    return context


@collect_auth
def redirect_unsupported_institution(auth):
    """
    Sends user back to the "Unsupported Institution" page on CAS. Logs user out if they are
    already logged in.

    HTTP Method: GET

    :param auth: the authentication context
    :return
    """

    cas_unsupp_inst_url = cas.get_login_url('', campaign='unsupportedinstitution')

    # if users are logged in, log them out and redirect back to this page
    if auth.logged_in:
        return auth_logout(redirect_url=cas_unsupp_inst_url)

    return redirect(cas_unsupp_inst_url)


def forgot_password_post():
    """Dispatches to ``_forgot_password_post`` passing non-institutional user mail template
    and reset action."""
    return _forgot_password_post(mail_template=mails.FORGOT_PASSWORD,
                                 reset_route='reset_password_get')


def forgot_password_institution_post():
    """Dispatches to `_forgot_password_post` passing institutional user mail template, reset
    action, and setting the ``institutional`` flag."""
    return _forgot_password_post(mail_template=mails.FORGOT_PASSWORD_INSTITUTION,
                                 reset_route='reset_password_institution_get',
                                 institutional=True)


def _forgot_password_post(mail_template, reset_route, institutional=False):
    """
    View for user to submit forgot password form (standard or institutional).  Validates submitted
    form and sends reset-password link via email if valid.  If user has submitted another password
    reset request recently, declines to create a new one and asks the user to not submit again for
    awhile.

    Standard and institutional forgot-password requests behave similarly but use slightly different
    language and interfaces. When an institution is deactivated, the user should be given the
    opportunity to reclaim their account. CAS co-ops the forgot-password functionality to send a
    "set a new password" email link to the institutional user.  The language of the email has been
    adjusted from the standard context, the response html the status message from the reset
    action is displayed as regular text, and the password form is not shown.

    HTTP Method: POST
    :return {}
    """

    form = ForgotPasswordForm(request.form, prefix='forgot_password')

    if not form.validate():
        # Don't go anywhere
        forms.push_errors_to_status(form.errors)
    else:
        email = form.email.data
        status_message = ('If there is an OSF account associated with {0}, an email with instructions on how to '
                          'reset the OSF password has been sent to {0}. If you do not receive an email and believe '
                          'you should have, please contact OSF Support. ').format(email)
        kind = 'success'
        # check if the user exists
        user_obj = get_user(email=email)
        if user_obj:
            # rate limit forgot_password_post
            if not throttle_period_expired(user_obj.email_last_sent, settings.SEND_EMAIL_THROTTLE):
                status_message = 'You have recently requested to change your password. Please wait a few minutes ' \
                                 'before trying again.'
                kind = 'error'
            # TODO [OSF-6673]: Use the feature in [OSF-6998] for user to resend claim email.
            elif user_obj.is_active:
                # new random verification key (v2)
                user_obj.verification_key_v2 = generate_verification_key(verification_type='password')
                user_obj.email_last_sent = timezone.now()
                user_obj.save()
                reset_link = furl.urljoin(
                    settings.DOMAIN,
                    web_url_for(
                        reset_route,
                        uid=user_obj._id,
                        token=user_obj.verification_key_v2['token']
                    )
                )
                mails.send_mail(
                    to_addr=email,
                    mail=mail_template,
                    reset_link=reset_link,
                    can_change_preferences=False,
                )

        # institutional forgot password page displays the message as main text, not as an alert
        if institutional:
            # pass isError instead of kind to template to decouple python error flag from template's
            # css class
            return {'message': status_message, 'isError': (kind == 'error'),
                    'institutional': institutional}

        status.push_status_message(status_message, kind=kind, trust=False)

    return {}


def login_and_register_handler(auth, login=True, campaign=None, next_url=None, logout=None):
    """
    Non-view helper to handle `login` and `register` requests.

    :param auth: the auth context
    :param login: `True` if `GET /login`, `False` if `GET /register`
    :param campaign: a target campaign defined in `auth.campaigns`
    :param next_url: the service url for CAS login or redirect url for OSF
    :param logout: used only for `claim_user_registered`
    :return: data object that contains actions for `auth_register` and `auth_login`
    :raises: http_status.HTTP_400_BAD_REQUEST
    """

    # Only allow redirects which are relative root or full domain. Disallows external redirects.
    if next_url and not validate_next_url(next_url):
        raise HTTPError(http_status.HTTP_400_BAD_REQUEST)

    data = {
        'status_code': http_status.HTTP_302_FOUND if login else http_status.HTTP_200_OK,
        'next_url': next_url,
        'campaign': None,
        'must_login_warning': False,
    }

    # login or register with campaign parameter
    if campaign:
        if validate_campaign(campaign):
            # GET `/register` or '/login` with `campaign=institution`
            # unlike other campaigns, institution login serves as an alternative for authentication
            if campaign == 'institution':
                if next_url is None:
                    next_url = web_url_for('dashboard', _absolute=True)
                data['status_code'] = http_status.HTTP_302_FOUND
                if auth.logged_in:
                    data['next_url'] = next_url
                else:
                    data['next_url'] = cas.get_login_url(next_url, campaign='institution')
            # for non-institution campaigns
            else:
                destination = next_url if next_url else campaigns.campaign_url_for(campaign)
                if auth.logged_in:
                    # if user is already logged in, go to the campaign landing page
                    data['status_code'] = http_status.HTTP_302_FOUND
                    data['next_url'] = destination
                else:
                    # if user is logged out, go to the osf register page with campaign context
                    if login:
                        # `GET /login?campaign=...`
                        data['next_url'] = web_url_for('auth_register', campaign=campaign, next=destination)
                    else:
                        # `GET /register?campaign=...`
                        data['campaign'] = campaign
                        if campaigns.is_proxy_login(campaign):
                            data['next_url'] = web_url_for(
                                'auth_login',
                                next=destination,
                                _absolute=True
                            )
                        else:
                            data['next_url'] = destination
        else:
            # invalid campaign, inform sentry and redirect to non-campaign sign up or sign in
            redirect_view = 'auth_login' if login else 'auth_register'
            data['status_code'] = http_status.HTTP_302_FOUND
            data['next_url'] = web_url_for(redirect_view, campaigns=None, next=next_url)
            data['campaign'] = None
            sentry.log_message(
                '{} is not a valid campaign. Please add it if this is a new one'.format(campaign)
            )
    # login or register with next parameter
    elif next_url:
        # TODO - logout is no longer used by claim_user_registered, see [#PLAT-1151]
        if logout:
            # handle `claim_user_registered`
            data['next_url'] = next_url
            if auth.logged_in:
                # log user out and come back
                data['status_code'] = 'auth_logout'
            else:
                # after logout, land on the register page with "must_login" warning
                data['status_code'] = http_status.HTTP_200_OK
                data['must_login_warning'] = True
        elif auth.logged_in:
            # if user is already logged in, redirect to `next_url`
            data['status_code'] = http_status.HTTP_302_FOUND
            data['next_url'] = next_url
        elif login:
            # `/login?next=next_url`: go to CAS login page with current request url as service url
            data['status_code'] = http_status.HTTP_302_FOUND
            data['next_url'] = cas.get_login_url(request.url)
        else:
            # `/register?next=next_url`: land on OSF register page with request url as next url
            data['status_code'] = http_status.HTTP_200_OK
            data['next_url'] = request.url
    else:
        # `/login/` or `/register/` without any parameter
        if auth.logged_in:
            data['status_code'] = http_status.HTTP_302_FOUND
        data['next_url'] = web_url_for('dashboard', _absolute=True)

    return data


@collect_auth
def auth_login(auth):
    """
    View (no template) for OSF Login.
    Redirect user based on `data` returned from `login_and_register_handler`.

    `/login` only takes valid campaign, valid next, or no query parameter
    `login_and_register_handler()` handles the following cases:
        if campaign and logged in, go to campaign landing page (or valid next_url if presents)
        if campaign and logged out, go to campaign register page (with next_url if presents)
        if next_url and logged in, go to next url
        if next_url and logged out, go to cas login page with current request url as service parameter
        if none, go to `/dashboard` which is decorated by `@must_be_logged_in`

    :param auth: the auth context
    :return: redirects
    """

    campaign = request.args.get('campaign')
    next_url = request.args.get('next')

    data = login_and_register_handler(auth, login=True, campaign=campaign, next_url=next_url)
    if data['status_code'] == http_status.HTTP_302_FOUND:
        return redirect(data['next_url'])


@collect_auth
@ember_flag_is_active(features.EMBER_AUTH_REGISTER)
def auth_register(auth):
    """
    View for OSF register. Land on the register page, redirect or go to `auth_logout`
    depending on `data` returned by `login_and_register_handler`.

    `/register` only takes a valid campaign, a valid next, the logout flag or no query parameter
    `login_and_register_handler()` handles the following cases:
        if campaign and logged in, go to campaign landing page (or valid next_url if presents)
        if campaign and logged out, go to campaign register page (with next_url if presents)
        if next_url and logged in, go to next url
        if next_url and logged out, go to cas login page with current request url as service parameter
        if next_url and logout flag, log user out first and then go to the next_url
        if none, go to `/dashboard` which is decorated by `@must_be_logged_in`

    :param auth: the auth context
    :return: land, redirect or `auth_logout`
    :raise: http_status.HTTP_400_BAD_REQUEST
    """

    context = {}
    # a target campaign in `auth.campaigns`
    campaign = request.args.get('campaign')
    # the service url for CAS login or redirect url for OSF
    next_url = request.args.get('next')
    # TODO: no longer used for `claim_user_registered`, see [#PLAT-1151]
    logout = request.args.get('logout')

    # logout must have next_url
    if logout and not next_url:
        raise HTTPError(http_status.HTTP_400_BAD_REQUEST)

    data = login_and_register_handler(auth, login=False, campaign=campaign, next_url=next_url, logout=logout)

    # land on register page
    if data['status_code'] == http_status.HTTP_200_OK:
        if data['must_login_warning']:
            status.push_status_message(language.MUST_LOGIN, trust=False)
        destination = cas.get_login_url(data['next_url'])
        # "Already have and account?" link
        context['non_institution_login_url'] = destination
        # "Sign In" button in navigation bar, overwrite the default value set in routes.py
        context['login_url'] = destination
        # "Login through your institution" link
        context['institution_login_url'] = cas.get_login_url(data['next_url'], campaign='institution')
        context['preprint_campaigns'] = {k._id + '-preprints': {
            'id': k._id,
            'name': k.name,
            'logo_path': k.get_asset_url('square_color_no_transparent')
        } for k in PreprintProvider.objects.all() if k._id != 'osf'}
        context['campaign'] = data['campaign']
        return context, http_status.HTTP_200_OK
    # redirect to url
    elif data['status_code'] == http_status.HTTP_302_FOUND:
        return redirect(data['next_url'])
    # go to other views
    elif data['status_code'] == 'auth_logout':
        return auth_logout(redirect_url=data['next_url'])

    raise HTTPError(http_status.HTTP_400_BAD_REQUEST)

@collect_auth
def auth_logout(auth, redirect_url=None, next_url=None):
    """
    Log out, delete current session and remove OSF cookie.
    If next url is valid and auth is logged in, redirect to CAS logout endpoint with the current request url as service.
    If next url is valid and auth is logged out, redirect directly to the next url.
    Otherwise, redirect to CAS logout or login endpoint with redirect url as service.
    The CAS logout endpoint which clears sessions and cookies for CAS and Shibboleth.
    HTTP Method: GET

    Note 1: OSF tells CAS where it wants to be redirected back after successful logout. However, CAS logout flow may not
    respect this url if user is authenticated through remote identity provider.
    Note 2: The name of the query parameter is `next`, `next_url` is used to avoid python reserved word.

    :param auth: the authentication context
    :param redirect_url: url to DIRECTLY redirect after CAS logout, default is `OSF/goodbye`
    :param next_url: url to redirect after OSF logout, which is after CAS logout
    :return: the response
    """

    # For `?next=`:
    #   takes priority
    #   the url must be a valid OSF next url,
    #   the full request url is set to CAS service url,
    #   does not support `reauth`
    # For `?redirect_url=`:
    #   the url must be valid CAS service url
    #   the redirect url is set to CAS service url.
    #   support `reauth`

    # logout/?next=<an OSF verified next url>
    next_url = next_url or request.args.get('next', None)
    if next_url and validate_next_url(next_url):
        cas_logout_endpoint = cas.get_logout_url(request.url)
        if auth.logged_in:
            resp = redirect(cas_logout_endpoint)
        else:
            resp = redirect(next_url)
    # logout/ or logout/?redirect_url=<a CAS verified redirect url>
    else:
        redirect_url = redirect_url or request.args.get('redirect_url') or web_url_for('goodbye', _absolute=True)
        # set redirection to CAS log out (or log in if `reauth` is present)
        if 'reauth' in request.args:
            cas_endpoint = cas.get_login_url(redirect_url)
        else:
            cas_endpoint = cas.get_logout_url(redirect_url)
        resp = redirect(cas_endpoint)

    # perform OSF logout
    osf_logout()

    # set response to delete OSF cookie
    resp.delete_cookie(settings.COOKIE_NAME, domain=settings.OSF_COOKIE_DOMAIN)

    return resp


def auth_email_logout(token, user):
    """
    When a user is adding an email or merging an account, add the email to the user and log them out.
    """

    redirect_url = cas.get_logout_url(service_url=cas.get_login_url(service_url=web_url_for('index', _absolute=True)))
    try:
        unconfirmed_email = user.get_unconfirmed_email_for_token(token)
    except InvalidTokenError:
        raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data={
            'message_short': 'Bad token',
            'message_long': 'The provided token is invalid.'
        })
    except ExpiredTokenError:
        status.push_status_message('The private link you used is expired.')
        raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data={
            'message_short': 'Expired link',
            'message_long': 'The private link you used is expired.'
        })
    try:
        user_merge = OSFUser.objects.get(emails__address=unconfirmed_email)
    except OSFUser.DoesNotExist:
        user_merge = False
    if user_merge:
        remove_sessions_for_user(user_merge)
    user.email_verifications[token]['confirmed'] = True
    user.save()
    remove_sessions_for_user(user)
    resp = redirect(redirect_url)
    resp.delete_cookie(settings.COOKIE_NAME, domain=settings.OSF_COOKIE_DOMAIN)
    return resp


@block_bing_preview
@collect_auth
def external_login_confirm_email_get(auth, uid, token):
    """
    View for email confirmation links when user first login through external identity provider.
    HTTP Method: GET

    When users click the confirm link, they are expected not to be logged in. If not, they will be logged out first and
    redirected back to this view. After OSF verifies the link and performs all actions, they will be automatically
    logged in through CAS and redirected back to this view again being authenticated.

    :param auth: the auth context
    :param uid: the user's primary key
    :param token: the verification token
    """

    user = OSFUser.load(uid)
    if not user:
        sentry.log_message('external_login_confirm_email_get::400 - Cannot find user')
        raise HTTPError(http_status.HTTP_400_BAD_REQUEST)

    destination = request.args.get('destination')
    if not destination:
        sentry.log_message('external_login_confirm_email_get::400 - bad destination')
        raise HTTPError(http_status.HTTP_400_BAD_REQUEST)

    # if user is already logged in
    if auth and auth.user:
        # if it is a wrong user
        if auth.user._id != user._id:
            return auth_logout(redirect_url=request.url)
        # if it is the expected user
        new = request.args.get('new', None)
        if destination in campaigns.get_campaigns():
            # external domain takes priority
            campaign_url = campaigns.external_campaign_url_for(destination)
            if not campaign_url:
                campaign_url = campaigns.campaign_url_for(destination)
            return redirect(campaign_url)
        if new:
            status.push_status_message(language.WELCOME_MESSAGE, kind='default', jumbotron=True, trust=True, id='welcome_message')
        return redirect(web_url_for('dashboard'))

    # token is invalid
    if token not in user.email_verifications:
        sentry.log_message('external_login_confirm_email_get::400 - bad token')
        raise HTTPError(http_status.HTTP_400_BAD_REQUEST)
    verification = user.email_verifications[token]
    email = verification['email']
    provider = list(verification['external_identity'].keys())[0]
    provider_id = list(verification['external_identity'][provider].keys())[0]
    # wrong provider
    if provider not in user.external_identity:
        sentry.log_message('external_login_confirm_email_get::400 - Auth error...wrong provider')
        raise HTTPError(http_status.HTTP_400_BAD_REQUEST)
    external_status = user.external_identity[provider][provider_id]

    try:
        ensure_external_identity_uniqueness(provider, provider_id, user)
    except ValidationError as e:
        sentry.log_message('external_login_confirm_email_get::403 - Validation Error')
        raise HTTPError(http_status.HTTP_403_FORBIDDEN, e.message)

    if not user.is_registered:
        user.register(email)

    if not user.emails.filter(address=email.lower()):
        user.emails.create(address=email.lower())

    user.date_last_logged_in = timezone.now()
    user.external_identity[provider][provider_id] = 'VERIFIED'
    user.social[provider.lower()] = provider_id
    del user.email_verifications[token]
    user.verification_key = generate_verification_key()
    user.save()

    service_url = request.url

    if external_status == 'CREATE':
        mails.send_mail(
            to_addr=user.username,
            mail=mails.WELCOME,
            user=user,
            domain=settings.DOMAIN,
            osf_support_email=settings.OSF_SUPPORT_EMAIL,
            storage_flag_is_active=storage_i18n_flag_active(),
        )
        service_url += '&{}'.format(urlencode({'new': 'true'}))
    elif external_status == 'LINK':
        mails.send_mail(
            user=user,
            to_addr=user.username,
            mail=mails.EXTERNAL_LOGIN_LINK_SUCCESS,
            external_id_provider=provider,
            can_change_preferences=False,
        )

    # redirect to CAS and authenticate the user with the verification key
    return redirect(cas.get_login_url(
        service_url,
        username=user.username,
        verification_key=user.verification_key
    ))


@block_bing_preview
@collect_auth
def confirm_email_get(token, auth=None, **kwargs):
    """