# coding=utf-8
"""
Login-related views (login / logout / password reminder / ...).

Notes:
- Uses code copy/pasted (and modified) from Flask-Security
"""
from __future__ import absolute_import, print_function, division

from datetime import datetime
import random
import string
from urlparse import urlparse, urljoin

from flask import (
  render_template, request, redirect, url_for, flash, current_app)
from flask.json import jsonify
from flask.globals import _request_ctx_stack
from flask_login import (
  login_user, logout_user, user_logged_in, user_logged_out,)
from flask_security.utils import md5
from flask_mail import Message
from sqlalchemy import sql
from sqlalchemy.orm.exc import NoResultFound
from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadSignature

from abilian.i18n import _, render_template_i18n
from abilian.core.extensions import db, csrf
from abilian.core.models.subjects import User
from abilian.web.blueprints import Blueprint
from abilian.services.security import Anonymous
from .models import LoginSession


__all__ = []

login = Blueprint("login", __name__,
                  url_prefix="/user",
                  allowed_roles=Anonymous,
                  template_folder='templates')
route = login.route


#
# Login / Logout
#
@route("/login")
def login_form():
  """Display the login form."""
  next_url = get_redirect_target()
  return render_template("login/login.html", next_url=next_url)


def do_login(form):
  email = form.get('email', "").lower()
  password = form.get('password')
  next_url = form.get('next', u'')
  res = dict(username=email, email=email, next_url=next_url)

  if not email or not password:
    res['error'] = _(u"You must provide your email and password.")
    res['code'] = 401
    return res

  try:
    user = User.query\
        .filter(sql.func.lower(User.email) == email,
                User.can_login == True)\
        .one()
  except NoResultFound:
    res['error'] = _(u"Sorry, we couldn't find an account for "
                     u"email '{email}'.").format(email=email)
    res['code'] = 401
    return res

  if user and not user.authenticate(password):
    res['error'] = _(u"Sorry, wrong password.")
    res['code'] = 401
    return res

  # Login successful
  login_user(user)
  res['user'] = user
  res['email'] = user.email
  return res


@csrf.exempt
@route("/login", methods=['POST'])
def login_post():
  res = do_login(request.form)
  if 'error' in res:
    code = res.pop('code')
    flash(res['error'], 'error')
    return (render_template("login/login.html", **res), code)

  return redirect_back(url=request.url_root)


@csrf.exempt
@route("/api/login", methods=['POST'])
def login_json():
  res = do_login(request.json)
  code = None

  if 'error' in res:
    code = res.pop('code')
  else:
    user = res.pop('user')
    res['fullname'] = user.name

  response = jsonify(**res)
  if code:
    response.status_code = code
  return response


@csrf.exempt
@route("/logout", methods=['GET', 'POST'])
def logout():
  logout_user()
  return redirect(request.url_root)


@csrf.exempt
@route("/api/logout", methods=['POST'])
def logout_json():
  logout_user()
  return  u'Logged out, ok.', 200


#
# Code to deal with forgotten passwords or initial password generation
# Note: most of this code is stolen (copy/pasted) from Flask-Security, with a
# few modifications.
#
@route("/forgotten_pw")
def forgotten_pw_form():
  return render_template("login/forgotten_password.html")


@route("/forgotten_pw", methods=['POST'])
@csrf.exempt
def forgotten_pw(new_user=False):
  """ reset password for users who have already activated their accounts.
  """
  email = request.form.get('email', "").lower()

  action = request.form.get('action')
  if action == "cancel":
    return redirect(url_for("login.login_form"))

  if not email:
    flash(_(u"You must provide your email address."), 'error')
    return render_template("login/forgotten_password.html")

  try:
    user = User.query\
        .filter(sql.func.lower(User.email) == email,
                User.can_login == True)\
        .one()
  except NoResultFound:
    flash(_(u"Sorry, we couldn't find an account for "
            "email '{email}'.").format(email=email), 'error')
    return render_template("login/forgotten_password.html"), 401

  if user.can_login and not user.password:
    user.set_password(random_password())
    db.session.commit()

  send_reset_password_instructions(user)
  flash(_("Password reset instructions have been sent to your email address."),
        'info')

  return redirect(url_for("login.login_form"))


@route("/reset_password/<token>")
def reset_password(token):
  expired, invalid, user = reset_password_token_status(token)
  if invalid:
    flash(_(u"Invalid reset password token."), "error")
  elif expired:
    flash(_(u"Password reset expired"), "error")
  if invalid or expired:
    return redirect(url_for('login.forgotten_pw'))

  return render_template_i18n("login/password_reset.html")


@route("/reset_password/<token>", methods=['POST'])
@csrf.exempt
def reset_password_post(token):
  action = request.form.get('action')
  if action == "cancel":
    return redirect(request.url_root)

  expired, invalid, user = reset_password_token_status(token)

  if invalid or not user:
    flash(_(u"Invalid reset password token."), "error")
  elif expired:
    flash(_(u"Password reset expired"), "error")
  if invalid or expired or not user:
    return redirect(url_for('login.forgotten_pw'))

  password = request.form.get('password')

  if not password:
    flash(_(u"You must provide a password."), "error")
    return redirect(url_for("login.reset_password_post", token=token))

  # TODO: check entropy
  if len(password) < 7:
    flash(_(u"Your new password must be at least 8 characters long"), "error")
    return redirect(url_for("login.reset_password_post", token=token))

  if password.lower() == password:
    flash(_(u"Your new password must contain upper case and lower case "
            "letters"), "error")
    return redirect(url_for("login.reset_password_post", token=token))

  if not len([x for x in password if x.isdigit()]) > 0:
    flash(_(u"Your new password must contain at least one digit"), "error")
    return redirect(url_for("login.reset_password_post", token=token))

  user.set_password(password)
  db.session.commit()

  flash(_(u"Your password has been reset. You can now login with your new password"),
        "info")

  return redirect(url_for("login.login_form", next_url=request.url_root))


def random_password():
  pw = []
  for i in range(0, 10):
    pw.append(random.choice(string.letters + string.digits))
  return pw


def get_serializer(name):
  """
  :type name: str
  """
  config = current_app.config
  secret_key = config.get('SECRET_KEY')
  salt = config.get('SECURITY_{}_SALT'.format(name.upper()))
  return URLSafeTimedSerializer(secret_key=secret_key, salt=salt)


def send_reset_password_instructions(user):
  """Sends the reset password instructions email for the specified user.

  :param user: The user to send the instructions to
  :type user: User
  """
  token = generate_reset_password_token(user)
  url = url_for('login.reset_password', token=token)
  reset_link = request.url_root[:-1] + url

  subject = _(u"Password reset instruction for {site_name}"
              ).format(site_name=current_app.config.get('SITE_NAME'))
  mail_template = 'password_reset_instructions'
  send_mail(subject, user.email, mail_template, user=user, reset_link=reset_link)


def generate_reset_password_token(user):
  """Generates a unique reset password token for the specified user.

  :param user: The user to work with
  :type user: User
  """
  data = [str(user.id), md5(user.password)]
  return get_serializer("reset").dumps(data)


def reset_password_token_status(token):
  """Returns the expired status, invalid status, and user of a password reset
  token. For example::

      expired, invalid, user = reset_password_token_status('...')

  :param token: The password reset token
  :type token: str
  """
  return get_token_status(token, 'reset', 'RESET_PASSWORD')


def get_token_status(token, serializer_name, max_age=None):
  serializer = get_serializer(serializer_name)
  #max_age = get_max_age(max_age)
  user, data = None, None
  expired, invalid = False, False

  try:
    data = serializer.loads(token, max_age=max_age)
  except SignatureExpired:
    d, data = serializer.loads_unsafe(token)
    expired = True
  except BadSignature:
    invalid = True

  if data:
    user = User.query.get(data[0])

  expired = expired and (user is not None)
  return expired, invalid, user


def send_mail(subject, recipient, template, **context):
  """Send an email using the Flask-Mail extension.

  :param subject: Email subject
  :param recipient: Email recipient
  :param template: The name of the email template
  :param context: The context to render the template with
  """

  config = current_app.config
  sender = config['MAIL_SENDER']
  msg = Message(subject,
                sender=sender,
                recipients=[recipient])

  ctx = ('login/email', template)
  msg.body = render_template_i18n('%s/%s.txt' % ctx, **context)
  #msg.html = render_template('%s/%s.html' % ctx, **context)

  mail = current_app.extensions.get('mail')
  current_app.logger.debug("Sending mail...")
  mail.send(msg)


#
# Logging
#
def log_session_start(app, user):
  session = LoginSession.new()
  db.session.add(session)
  db.session.commit()


def log_session_end(app, user):
  session = LoginSession.query.get_active_for(user)
  if session:
    session.ended_at = datetime.utcnow()
    db.session.commit()


user_logged_in.connect(log_session_start)
user_logged_out.connect(log_session_end)


# login redirect utilities
#  from http://flask.pocoo.org/snippets/62/
def is_safe_url(target):
  ref_url = urlparse(request.host_url)
  test_url = urlparse(urljoin(request.url_root, target))
  return test_url.scheme in ('http', 'https') and \
      ref_url.netloc == test_url.netloc


def check_for_redirect(target):
  target = urljoin(request.url_root, target)
  url = urlparse(target)
  reqctx = _request_ctx_stack.top
  try:
    endpoint, ignored = reqctx.url_adapter.match(url.path, 'GET')
    if '.' in endpoint and endpoint.rsplit('.', 1)[0] == 'login':
      # don't redirect to any login view after successful login
      return None
  except Exception as e:
    # exceptions may happen if route is not found for example
    pass

  return target


def get_redirect_target():
  for target in (request.values.get('next'), request.referrer):
    if not target:
      continue
    if is_safe_url(target):
      return check_for_redirect(target)


def redirect_back(endpoint=None, url=None, **values):
  target = request.form.get('next')
  if not target or not is_safe_url(target):
    if endpoint:
      target = url_for(endpoint, **values)
    else:
      target = url
  return redirect(check_for_redirect(target))
