# coding=utf-8
"""
"""
from __future__ import absolute_import, print_function, division

import logging
from datetime import datetime, timedelta

from werkzeug.exceptions import Forbidden
from flask import current_app, g, request, url_for, redirect
from flask_login import (
  current_user, user_logged_out, user_logged_in, login_user
)
from flask_babel import lazy_gettext as _l

from abilian.services import Service, ServiceState
from abilian.core.signals import user_loaded
from abilian.core.models.subjects import User
from abilian.core.extensions import db, login_manager
from abilian.web.action import actions, DynamicIcon
from abilian.web.nav import NavItem, NavGroup

from .views import login as login_views
from .models import LoginSession


__all__ = ['AuthService', 'user_menu']

logger = logging.getLogger(__name__)


def is_anonymous(context):
  return current_user.is_anonymous()


def is_authenticated(context):
  return not is_anonymous(context)


def _user_photo_endpoint():
  from abilian.web.views import images # late import: avoid circular import
  return images.user_url_args(current_user, 16)[0]

def _user_photo_icon_args(icon, url_args):
  from abilian.web.views import images  # late import avoid circular import
  return images.user_url_args(current_user, max(icon.width, icon.height))[1]

user_menu = NavGroup(
  'user', 'authenticated', title=lambda c: current_user.name,
  icon=DynamicIcon(endpoint=_user_photo_endpoint,
                   css='avatar', size=20,
                   url_args=_user_photo_icon_args),
  condition=is_authenticated,
  items=(
      NavItem('user', 'logout', title=_l(u'Logout'), icon='log-out',
              url=lambda context: url_for('login.logout'),
              divider=True),
  ))


_ACTIONS = (
  NavItem('user', 'login', title=_l(u'Login'), icon='log-in',
          url=lambda context: url_for('login.login_form'),
          condition=is_anonymous),
  user_menu,
)

class AuthServiceState(ServiceState):
  """
  State class for :class:`AuthService`
  """
  def __init__(self, *args, **kwargs):
    super(AuthServiceState, self).__init__(*args, **kwargs)
    self.bp_access_controllers = {None: []}
    self.endpoint_access_controllers = {}


  def add_bp_access_controller(self, blueprint, func):
    self.bp_access_controllers.setdefault(blueprint, []).append(func)

  def add_endpoint_access_controller(self, endpoint, func):
    self.endpoint_access_controllers.setdefault(endpoint, []).append(func)


class AuthService(Service):
  name = 'auth'
  AppStateClass = AuthServiceState

  def init_app(self, app):
    login_manager.init_app(app)
    login_manager.login_view = 'login.login_form'
    Service.init_app(self, app)
    self.login_url_prefix = app.config.get('LOGIN_URL', '/user')
    app.before_request(self.do_access_control)
    app.before_request(self.update_user_session_data)
    user_logged_in.connect(self.user_logged_in, sender=app)
    user_logged_out.connect(self.user_logged_out, sender=app)
    app.register_blueprint(login_views, url_prefix=self.login_url_prefix)
    with app.app_context():
      actions.register(*_ACTIONS)

  @login_manager.user_loader
  def load_user(user_id):
    try:
      user = User.query.get(user_id)

      if not user or not user.can_login:
        # if a user is edited and should not have access any more, this will
        # ensure he cannot continue if he had an active session
        return None
    except:
      logger.warning("Error during login.", exc_info=True)
      session = current_app.db.session()
      if not session.is_active:
        # session is not usable, rollback should restore a usable session
        session.rollback()

      return None

    app = current_app._get_current_object()
    app.services[AuthService.name].user_logged_in(app, user)
    user_loaded.send(app, user=user)
    return user

  def user_logged_in(self, app, user):
    # `g.user` is used as `current_user`, but `current_user` is actually looking
    # for `request.user` whereas `g` is on app local stack.
    #
    # `g.logged_user` is the actual user. In the future for example we may allow
    # a manager to see site as another user (impersonate), or propose a "see as
    # anonymous" function
    g.user = g.logged_user = user
    is_anonymous = user is None or user.is_anonymous()
    security = app.services.get('security')
    g.is_manager = (user
                    and not is_anonymous
                    and ((security.has_role(user, 'admin')
                          or security.has_role(user, 'manager'))))

  def user_logged_out(self, app, user):
    del g.user
    del g.logged_user
    del g.is_manager

  def redirect_to_login(self, next_url=True):
    kw = {}
    if next_url is not False:
      kw['next'] = request.url if next_url is True else next_url

    return redirect(url_for(login_manager.login_view, **kw))

  def do_access_control(self):
    """
    `before_request` handler to check if user should be redirected to login
    page.
    """
    if current_app.testing and current_app.config.get("NO_LOGIN"):
      # Special case for tests
      user = User.query.get(0)
      login_user(user, False, True)
      return

    state = self.app_state
    user = current_user._get_current_object()
    roles = frozenset(current_app.services['security'].get_roles(user))
    endpoint = request.endpoint
    bp = request.blueprint

    controllers = []
    controllers.extend(state.bp_access_controllers.get(None, []))

    if bp and bp in state.bp_access_controllers:
      controllers.extend(state.bp_access_controllers[bp])

    if endpoint and endpoint in state.endpoint_access_controllers:
      controllers.extend(state.endpoint_access_controllers[endpoint])

    for ctrl in reversed(controllers):
      verdict = ctrl(user=user, roles=roles)
      if verdict is None:
        continue
      elif verdict is True:
        return
      else:
        if user.is_anonymous():
          return self.redirect_to_login()
        raise Forbidden()

    # default policy
    if current_app.config.get('PRIVATE_SITE') and user.is_anonymous():
      return self.redirect_to_login()


  def update_user_session_data(self):
    user = current_user
    if current_user.is_anonymous():
      return

    # Update last_active every 60 seconds only so as to not stress the database
    # too much.
    now = datetime.utcnow()
    if (user.last_active is None or
        (now - user.last_active) > timedelta(minutes=1)):
      user.last_active = now
      db.session.add(user)
      db.session.commit()

    refresh_login_session(user)


def refresh_login_session(user):
  now = datetime.utcnow()
  session = LoginSession.query.get_active_for(user)
  if not session:
    return

  from_now = now - session.last_active_at

  if from_now > timedelta(hours=1):
    session.ended_at = session.last_active_at
    session = LoginSession.new()
    db.session.add(session)
    db.session.commit()
  elif from_now > timedelta(minutes=1):
    session.last_active_at = now
    db.session.commit()
