# 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

from datetime import datetime

import random
import string
from urlparse import urlparse, urljoin

from sqlalchemy import sql
from sqlalchemy.orm.exc import NoResultFound
from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadSignature

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.ext.login import (
  login_user, logout_user, user_logged_in, user_logged_out,)
from flask.ext.security.utils import md5
from flask.ext.mail import Message

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):
  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
  """
  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
  """
  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
  """
  return get_token_status(token, 'reset', 'RESET_PASSWORD')


def get_token_status(token, serializer, max_age=None):
  serializer = get_serializer(serializer)
  #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))
