"""
Subject classes (i.e. people, groups, etc.).

See ICOM-ics-v1.0 "Subject Branch".

TODO: I'm not a big fan of the "subject" name. Could be replaced by something
else, like "people" or "principal" ?
"""
from __future__ import absolute_import

from abc import ABCMeta, abstractmethod, abstractproperty
import bcrypt
from datetime import datetime, timedelta

from flask.ext.login import UserMixin

from sqlalchemy.orm import relationship, backref, deferred
from sqlalchemy.orm.query import Query
from sqlalchemy.schema import Column, Table, ForeignKey, UniqueConstraint
from sqlalchemy.types import Integer, UnicodeText, LargeBinary, Boolean, DateTime, Text

from abilian.core.extensions import db
from .base import IdMixin, TimestampedMixin, Indexable, SEARCHABLE, SYSTEM

__all__ = ['User', 'Group', 'Principal']


# Tables for many-to-many relationships
following = Table(
  'following', db.Model.metadata,
  Column('follower_id', Integer, ForeignKey('user.id')),
  Column('followee_id', Integer, ForeignKey('user.id')),
  UniqueConstraint('follower_id', 'followee_id'),
)
membership = Table(
  'membership', db.Model.metadata,
  Column('user_id', Integer, ForeignKey('user.id')),
  Column('group_id', Integer, ForeignKey('group.id')),
  UniqueConstraint('user_id', 'group_id'),
)

# Should not be needed (?)
administratorship = Table(
  'administratorship', db.Model.metadata,
  Column('user_id', Integer, ForeignKey('user.id')),
  Column('group_id', Integer, ForeignKey('group.id')),
  UniqueConstraint('user_id', 'group_id'),
)


class PasswordStrategy(object):
  """

  """
  __metaclass__ = ABCMeta

  @abstractproperty
  def name(self):
    """
    Strategy name.
    """

  @abstractmethod
  def authenticate(self, user, password):
    """
    Predicate to tell wether password match user's or not.
    """

  @abstractmethod
  def process(self, user, password):
    """
    Return a string to be stored as user password
    """

class ClearPasswordStrategy(PasswordStrategy):
  """
  Don't encrypt at all.

  This strategy should not ever be used elsewhere than in tests. It's useful
  in tests since a hash like bcrypt is designed to be slow.
  """
  @property
  def name(self):
    return "clear"

  def authenticate(self, user, password):
    return user.password == password

  def process(self, user, password):
    if not isinstance(password, unicode):
      password = password.decode('utf-8')
    return password


class BcryptPasswordStrategy(PasswordStrategy):
  """
  Hash passwords using bcrypt.
  """

  @property
  def name(self):
    return 'bcrypt'

  def authenticate(self, user, password):
    current_passwd = user.password
    # crypt work only on str, not unicode
    if isinstance(current_passwd, unicode):
      current_passwd = current_passwd.encode('utf-8')
    if isinstance(password, unicode):
      password = password.encode('utf-8')

    return bcrypt.hashpw(password, current_passwd) == current_passwd


  def process(self, user, password):
    if isinstance(password, unicode):
      password = password.encode('utf-8')

    return bcrypt.hashpw(password, bcrypt.gensalt()).decode('utf-8')


class UserQuery(Query):
  def get_by_email(self, email):
    return self.filter_by(email=email).one()


class Principal(IdMixin, TimestampedMixin, Indexable):
  """A principal is either a User or a Group."""
  pass


class User(Principal, UserMixin, db.Model):
  __tablename__ = 'user'
  __editable__ = ['first_name', 'last_name', 'email', 'password']
  __exportable__ = __editable__ + ['created_at', 'updated_at', 'id']

  __password_strategy__ = BcryptPasswordStrategy()

  entity_type = u'{}.{}'.format(__module__, 'User')

  query_class = UserQuery

  # Basic information
  first_name = Column(UnicodeText, info=SEARCHABLE)
  last_name = Column(UnicodeText, info=SEARCHABLE)
  # Should we add gender, salutation ?

  # System information
  locale = Column(Text)

  email = Column(UnicodeText, nullable=False)
  can_login = Column(Boolean, nullable=False, default=True)
  password = Column(UnicodeText, default=u"*",
                    info={'audit_hide_content': True})

  photo = deferred(Column(LargeBinary))

  last_active = Column(DateTime, info=SYSTEM)

  __table_args__ = (UniqueConstraint('email'),)

  followers = relationship("User", secondary=following,
                           primaryjoin=('User.id == following.c.follower_id'),
                           secondaryjoin=('User.id == following.c.followee_id'),
                           backref='followees')

  def __init__(self, password=None, **kwargs):
    Principal.__init__(self)
    UserMixin.__init__(self)
    db.Model.__init__(self, **kwargs)

    if self.can_login and password is not None:
      self.set_password(password)

  def authenticate(self, password):
    if self.password and self.password != "*":
      return self.__password_strategy__.authenticate(self, password)
    else:
      return False

  def set_password(self, password):
    """Encrypts and sets password."""
    self.password = self.__password_strategy__.process(self, password)

  def follow(self, followee):
    if followee == self:
      raise Exception("User can't follow self")
    self.followees.append(followee)

  def unfollow(self, followee):
    if followee == self:
      raise Exception("User can't follow self")
    i = self.followees.index(followee)
    del self.followees[i]

  def join(self, group):
    if not group in self.groups:
      self.groups.append(group)

  def leave(self, group):
    if group in self.groups:
      del self.groups[self.groups.index(group)]

  #
  # Boolean properties
  #
  def is_following(self, other):
    return other in self.followees

  def is_member_of(self, group):
    return self in group.members

  def is_admin_of(self, group):
    return self in group.admins

  @property
  def is_online(self):
    if self.last_active is None:
      return False
    return datetime.utcnow() - self.last_active <= timedelta(0, 60)

  #
  # Other properties
  #
  @property
  def name(self):
    name = u'{first_name} {last_name}'.format(first_name=self.first_name or u'',
                                              last_name=self.last_name or u'')
    return name.strip() or u'Unknown'

  def __unicode__(self):
    return self.name

  def __repr__(self):
    cls = self.__class__
    return '<{mod}.{cls} id={id} email={email} at 0x{addr:x}>'.format(
      mod=cls.__module__, cls=cls.__name__,
      id=repr(self.id), email=repr(self.email), addr=id(self)
      )


class Group(Principal, db.Model):
  __indexable__ = False
  __tablename__ = 'group'
  __editable__ = ['name', 'description']
  __exportable__ = __editable__ + ['created_at', 'updated_at', 'id']

  entity_type = u'{}.{}'.format(__module__, 'Group')

  name = Column(UnicodeText, nullable=False, info=SEARCHABLE)
  description = Column(UnicodeText, info=SEARCHABLE)

  members = relationship("User", secondary=membership,
                         backref=backref('groups', lazy='lazy'))
  admins = relationship("User", secondary=administratorship)

  photo = deferred(Column(LargeBinary))

  public = Column(Boolean, default=False, nullable=False)
