# coding=utf-8
"""
"""
from __future__ import absolute_import
from datetime import datetime
from itertools import chain
from markupsafe import Markup
import pytz

import sqlalchemy as sa
from sqlalchemy.orm.attributes import NO_VALUE

from werkzeug.routing import BuildError
from flask import request, render_template, render_template_string, \
  get_template_attribute
from flask.ext.babel import gettext as _, format_date, get_locale

from abilian.core.extensions import db
from abilian.core.models.subjects import User
from abilian.core.util import local_dt
from abilian.core.entities import Entity, all_entity_classes
from abilian.services.audit import AuditEntry
from abilian.services.security import SecurityAudit
from abilian.web.util import url_for
from abilian.web.views.base import JSONView

from ..panel import AdminPanel


def format_date_for_input(date):
  date_fmt = get_locale().date_formats['short'].pattern
  date_fmt = date_fmt.replace('MMMM', 'MM')\
      .replace('MMM', 'MM')  # force numerical months
  return format_date(date, date_fmt)


class JSONUserSearch(JSONView):
  """
  Search users by fullname
  """
  def data(self, q, *args, **kwargs):
    q = q.replace(u'%', u' ').strip().lower()

    if not q or len(q) < 2:
      abort(500)

    query = User.query
    lower = sa.sql.func.lower
    filters = []
    for part in q.split(u' '):
      filters.append(sa.sql.or_(lower(User.first_name).like(part + "%"),
                                lower(User.last_name).like(part + "%")))

    filters = sa.sql.and_(*filters) if len(filters) > 1 else filters[0]

    if u'@' in q:
      filters = sa.sql.or_(lower(User.email).like('%' + part + '%'),
                           filters)

    query = query.filter(filters)\
                 .order_by(User.last_name, User.first_name)

    result = {'results': [
      {'id': obj.id,
       'text': u'{} {} ({})'.format(obj.first_name, obj.last_name, obj.email)}
      for obj in query.values(User.id, User.first_name,
                              User.last_name, User.email)]
    }
    return result


class AuditPanel(AdminPanel):
  """Global audit log.

  Can receive one out of two possible GET parameter, datetime type:

  * before: events that happened immediately before 't' (less recent than 't')
  * after: events that happened immediately after 't' (more recent than 't')
  """
  id = 'audit'
  label = 'Audit trail'
  icon = 'list-alt'

  def install_additional_rules(self, add_url_rule):
    add_url_rule('/search_users',
                 view_func=JSONUserSearch.as_view('search_users'))

  def get(self):
    LIMIT = 30

    base_audit_q = AuditEntry.query
    base_security_q = SecurityAudit.query

    before = request.args.get('before')
    after = request.args.get('after')

    # filter on user
    filter_user = None
    user_id = request.args.get('user')
    if user_id:
      user_id = int(user_id)
      filter_user = User.query.get(user_id)
      base_audit_q = base_audit_q.filter(AuditEntry.user == filter_user)
      base_security_q = base_security_q.filter(
        SecurityAudit.manager == filter_user)

    # filter by types
    all_classes = sorted(all_entity_classes(), key=lambda c: c.__name__)
    all_types = set(e.entity_type for e in all_classes)
    filter_types = set(request.args.getlist('types')) & all_types
    if filter_types:
      if len(filter_types) == 1:
        t = list(filter_types)[0]
        audit_expr = AuditEntry.entity_type == t
        sec_expr = sa.sql.or_(SecurityAudit.object == None,
                              Entity._entity_type == t)
      else:
        audit_expr = AuditEntry.entity_type.in_(filter_types)
        sec_expr = sa.sql.or_(SecurityAudit.object == None,
                              Entity._entity_type.in_(filter_types))

      base_audit_q = base_audit_q.filter(audit_expr)
      base_security_q = base_security_q\
          .join(SecurityAudit.object)\
          .filter(sec_expr)\
          .reset_joinpoint()


    def after_query(q, model, date):
      return q.filter(model.happened_at > date)\
          .order_by(model.happened_at.asc())

    def before_query(q, model, date):
      return q.filter(model.happened_at < date)\
          .order_by(model.happened_at.desc())

    if after:
      after = datetime.strptime(after, u'%Y-%m-%dT%H:%M:%S.%f')
      audit_q = after_query(base_audit_q, AuditEntry, after)
      security_q = after_query(base_security_q, SecurityAudit, after)
    else:
      before = (datetime.strptime(before, u'%Y-%m-%dT%H:%M:%S.%f')
                if before else datetime.utcnow())
      audit_q = before_query(base_audit_q, AuditEntry, before)
      security_q = before_query(base_security_q, SecurityAudit, before)

    audit_entries = audit_q\
        .options(sa.orm.joinedload(AuditEntry.entity))\
        .limit(LIMIT)\
        .all()
    security_entries = security_q\
        .options(sa.orm.joinedload(SecurityAudit.object))\
        .limit(LIMIT)\
        .all()
    #audit_entries = []
    all_entries = list(
      chain((AuditEntryPresenter(e) for e in audit_entries),
            (SecurityEntryPresenter(e) for e in security_entries)))
    all_entries.sort()

    if after:
      all_entries = all_entries[:LIMIT]
    else:
      all_entries = all_entries[-LIMIT:]

    all_entries.reverse()  # event are presented from most to least recent

    # group entries by day
    entries = []
    day_entries = None
    current_day = None

    for e in all_entries:
      e_date = e.date

      if e_date.date() != current_day:
        current_day = e_date.date()
        day_entries = []
        entries.append((e_date.date(), day_entries))
      day_entries.append(e)

    top_date = u''
    lowest_date = u''

    if entries:
      # top_date and lowest_date are converted to naive datetime (from UTC), so
      # that isoformat does not include TZ shift (else we should fix strptime
      # above)
      top_date = entries[0][1][0].date.astimezone(pytz.utc).replace(tzinfo=None)
      lowest_date = entries[-1][1][-1].date.astimezone(pytz.utc)\
                                            .replace(tzinfo=None)

      after_queries = (
        after_query(base_audit_q, AuditEntry, top_date),
        after_query(base_security_q, SecurityAudit, top_date),)

      before_queries = (
        before_query(base_audit_q, AuditEntry, lowest_date),
        before_query(base_security_q, SecurityAudit, lowest_date).limit(1),)

      if not any(q.limit(1).first() is not None for q in after_queries):
        top_date = u''
      else:
        top_date = top_date.isoformat()

      if not any(q.first() is not None for q in before_queries):
        lowest_date = u''
      else:
        lowest_date = lowest_date.isoformat()

    current_date = None
    if entries:
      current_date = format_date_for_input(entries[0][0])

    # build prev/next urls
    url_params = {}
    if filter_user:
      url_params['user'] = filter_user.id
    if filter_types:
      url_params['types'] = list(filter_types)[0]

    return render_template(
        "admin/audit.html",
        entries=entries,
        filter_user=filter_user,
        all_classes=[(c.__name__, c.entity_type) for c in all_classes],
        filter_types=filter_types,
        url_params=url_params,
        current_date=current_date,
        top_date=top_date, lowest_date=lowest_date)


#
#  Presenters for audit entries listing
#
class BaseEntryPresenter(object):

  _USER_FMT = (u'<a href="{{ url_for("social.user", user_id=user.id) }}">'
               '{{ user.name }}</a>')
  _GROUP_FMT = (u'<a href="{{ url_for("social.group_home", group_id=group.id)'
                ' }}">{{ group.name }}</a>')

  def __init__(self, user, date):
    self.user = user
    self.date = local_dt(date)

  def __cmp__(self, other):
    return cmp(self.date, other.date)

  def __repr__(self):
    return '{}({}, {} @ {})'.format(
      self.__class__.__name__, repr(self.user), repr(self.date), id(self))

  @staticmethod
  def model(model_name):
    return db.Model._decl_class_registry.get(model_name)

  def render(self):
    raise NotImplementedError


class AuditEntryPresenter(BaseEntryPresenter):

  def __init__(self, entry):
    assert isinstance(entry, AuditEntry)
    super(AuditEntryPresenter, self).__init__(entry.user, entry.happened_at)
    self.entry = entry

  def render(self):
    render = render_template_string
    e = self.entry
    user = render(self._USER_FMT, user=e.user)
    self.changes = []
    # entity = e.entity_name or u''
    self.entity_deleted = e.entity is None
    entity_html = e.entity_name

    if not self.entity_deleted:
      try:
        entity_url = url_for(e.entity)
      except (BuildError, ValueError):
        pass
      else:
        entity_html = Markup(render(
          u'<a href="{{ url }}">{{ entity.path or entity.name }}</a>',
          url=entity_url,
          entity=e.entity))

    if e.type == 0:
      msg = _(u'{user} created {entity_type} {entity_id} "{entity}"')
    elif e.related or e.op == 1:
      msg = _(u'{user} made changes on {entity_type} {entity_id} "{entity}"')
      self.changes.extend(self.format_model_changes(e.changes))
    elif e.op == 2:
      msg = _(u'{user} has deleted {entity_type}: {entity_id} "{entity}"')
    else:
      raise Exception("Bad entry type: {}".format(e.type))

    self.msg = Markup(msg.format(user=user, entity=entity_html,
                                 entity_type=e.entity_type.rsplit('.', 1)[-1],
                                 entity_id=e.entity_id,))
    tmpl = get_template_attribute('admin/_macros.html', 'm_audit_entry')
    return tmpl(self)

  def format_model_changes(self, changes):
    formatted_items = []
    render = render_template_string
    for key, value in changes.items():
      key = render(u'<strong>{{ key }}</strong>', key=key)
      value_fmt = u'<em>{{ value }}</em>'

      if isinstance(value, dict):
        submodel_changes = u'\n'.join(
          u'<li>{}</li>'.format(msg) for msg in self.format_model_changes(value))
        submodel_changes = u'<ul>{}</ul>'.format(submodel_changes)
        change_msg = _(u'{key} changed:').format(key=key) + submodel_changes
      else:
        old_value, new_value = value
        if old_value and old_value is not NO_VALUE:
          old_value = render(value_fmt, value=old_value)
          change_msg = _(u'{key} changed from {old_value} to {new_value}')
        else:
          change_msg = _(u'{key} set to {new_value}')

        change_msg = change_msg.format(key=key, old_value=old_value,
                                       new_value=new_value)

      formatted_items.append(Markup(change_msg))
    return formatted_items


class SecurityEntryPresenter(BaseEntryPresenter):

  def __init__(self, entry):
    assert isinstance(entry, SecurityAudit)
    super(SecurityEntryPresenter, self).__init__(entry.manager, entry.happened_at)
    self.entry = entry

  def render(self):
    render = render_template_string
    e = self.entry

    manager = render(
      u'<img src="{{ url_for("social.user_mugshot", user_id=e.manager_id, '
      's=16) }}" alt="" />'
      '<a href="''{{ url_for("social.user", user_id=e.manager_id) }}">'
      '{{ e.manager.name }}</a>', e=e)

    if self.entry.user:
      principal = render(self._USER_FMT, user=self.entry.user)
    elif self.entry.group:
      principal = render(self._GROUP_FMT, group=self.entry.group)
    else:
      principal = u''

    entity = u''
    if e.object:
      entity = render(
        u'<a href="{{ url_for(entity) }}">{{ entity.path or entity.name }}</a>',
        entity=e.object)

      if e.op == e.SET_INHERIT:
        msg = _(u'{manager} has activated inheritance on {entity}')
      elif e.op == e.UNSET_INHERIT:
        msg = _(u'{manager} has deactivated inheritance on {entity}')
      elif e.op == e.GRANT:
        msg = _(u'{manager} has given role "{role}" to {principal} '
                'on {entity}')
      elif e.op == e.REVOKE:
        msg = _(u'{manager} has revoked role "{role}" from '
                '{principal} on {entity}')
      else:
        raise Exception("Invalid entity op: {}".format(e.op))
    else:
      if e.op == e.GRANT:
        msg = _(u'{manager} has given role "{role}" to {principal}')
      elif e.op == e.REVOKE:
        msg = _(u'{manager} has revoked role "{role}" from {principal}')
      else:
        raise Exception("Invalid entity op: {}".format(e.op))

    self.msg = Markup(msg.format(manager=manager,
                                 principal=principal,
                                 role=e.role,
                                 entity=entity))
    tmpl = get_template_attribute('admin/_macros.html', 'm_security_entry')
    return tmpl(self)
