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

import logging
import time
from datetime import datetime
from os.path import dirname, join

import sqlalchemy as sa
from blinker import ANY
from flask import current_app
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, \
    String, Unicode, UniqueConstraint, and_
from sqlalchemy.event import listens_for
from sqlalchemy.orm import backref, relation, relationship
from sqlalchemy.orm.attributes import OP_APPEND, OP_REMOVE

from abilian.core.entities import Entity
from abilian.core.extensions import db
from abilian.core.models import NOT_AUDITABLE, SEARCHABLE
from abilian.core.models.blob import Blob
from abilian.core.models.subjects import Group, User
from abilian.i18n import _l
from abilian.sbe.apps.documents.models import Folder
from abilian.sbe.apps.documents.repository import repository
from abilian.services.indexing import indexable_role
from abilian.services.security import Manager as MANAGER
from abilian.services.security import Reader as READER
from abilian.services.security import Writer as WRITER
from abilian.services.security import READ, WRITE, Admin, Permission, Role, \
    RoleType, security

from . import signals

logger = logging.getLogger(__name__)

MEMBER = Role('member', label=_l(u'role_member'), assignable=False)
VALID_ROLES = frozenset([READER, WRITER, MANAGER, MEMBER])


class Membership(db.Model):
    """Represents the membership of someone in a community.
    """
    __tablename__ = 'community_membership'

    id = Column(Integer, primary_key=True)

    user_id = Column(ForeignKey('user.id'), index=True, nullable=False)
    user = relationship(
        User,
        lazy='joined',
        backref=backref('communautes_membership', lazy='select'))

    community_id = Column(
        ForeignKey('community.id'), index=True, nullable=False)
    community = relationship('Community', lazy='joined')

    role = Column(RoleType())  # should be either 'member' or 'manager'

    __table_args__ = (UniqueConstraint('user_id', 'community_id'),)

    def __repr__(self):
        return u'<Membership user={}, community={}, role={}>'.format(
            repr(self.user), repr(self.community),
            str(self.role)).encode('utf-8')


def community_content(cls):
    """Class decorator to mark models considered as community content.

    This is required for proper indexation.
    """
    cls.is_community_content = True

    def community_slug(self):
        return self.community and self.community.slug

    cls.community_slug = property(community_slug)
    index_to = cls.__indexation_args__.setdefault('index_to', ())
    index_to += (('community_slug', ('community_slug',)),)
    cls.__indexation_args__['index_to'] = index_to

    def _indexable_roles_and_users(self):
        if not self.community:
            return []
        return indexable_roles_and_users(self.community)

    cls._indexable_roles_and_users = property(_indexable_roles_and_users)
    return cls


def indexable_roles_and_users(community):
    """Mixin to use to replace Entity._indexable_roles_and_users.

    Will be removed when communities are upgraded to use standard role based
    access (by setting permissions and using security service).
    """
    return u' '.join(indexable_role(user) for user in community.members)


class Community(Entity):
    """Ad-hoc objects that hold properties about a community.
    """
    __indexation_args__ = {}
    __indexation_args__.update(Entity.__indexation_args__)
    index_to = __indexation_args__.setdefault('index_to', ())
    index_to += (('id', ('id', 'community_id')),)
    __indexation_args__['index_to'] = index_to
    del index_to

    is_community_content = True

    # : A public description.
    description = Column(
        Unicode(500), default=u"", nullable=False, info=SEARCHABLE)

    #: An image or logo for this community.
    image_id = Column(ForeignKey(Blob.id), index=True)
    image = relationship(Blob, lazy='joined')

    #: The root folder for this community.
    folder_id = Column(
        Integer,
        ForeignKey(Folder.id, use_alter=True, name='fk_community_root_folder'),
        unique=True)
    folder = relation(
        Folder,
        single_parent=True,  # required for delete-orphan
        primaryjoin=(folder_id == Folder.id),
        cascade='all, delete-orphan',
        backref=backref('_community', lazy='select', uselist=False))

    #: The group this community is linked to, if any. Memberships are then
    #: reflected
    group_id = Column(ForeignKey(Group.id), nullable=True, unique=True)
    group = relation(Group, foreign_keys=group_id, lazy='select')

    #: Memberships for this community.
    memberships = relationship(Membership, cascade="all, delete-orphan")

    #: direct access to :class:`User` members
    members = relationship(
        User,
        secondary=Membership.__table__,
        viewonly=True,
        backref=backref('communities', lazy='select', viewonly=True))

    #: Number of members in this community.
    membership_count = Column(
        Integer, default=0, nullable=False, info=NOT_AUDITABLE)

    #: Number of documents in this community.
    document_count = Column(
        Integer, default=0, nullable=False, info=NOT_AUDITABLE)

    #: Last time something happened in this community
    last_active_at = Column(
        DateTime, default=datetime.utcnow, nullable=False, info=NOT_AUDITABLE)

    # Various features for this community:

    #: True if this community has a document management space
    has_documents = Column(Boolean, nullable=False, default=True)

    #: True if this community has a wiki
    has_wiki = Column(Boolean, nullable=False, default=True)

    #: True if this community has a forum
    has_forum = Column(Boolean, nullable=False, default=True)

    #: One of 'participative' or 'informative
    type = Column(String(20), nullable=False, default="informative")

    #: One of 'secret', 'public', 'open' (currently not used)
    visibility = Column(String(20), nullable=False, default="secret")

    #: Used for segmenting communities. Not used currently.
    category = Column(String(20), nullable=False, default="")

    #: True if regular members can send stuff by email to all members
    # Not used anymore.
    members_can_send_by_email = Column(Boolean, nullable=False, default=False)

    def __init__(self, **kw):
        self.has_documents = True
        self.membership_count = 0
        self.document_count = 0
        self.members_can_send_by_email = False
        Entity.__init__(self, **kw)
        if self.has_documents and not self.folder:
            # FIXME: this should be done in documents by using signals
            name = self.name
            if not name:
                # during creation, we may have to provide a temporary name for
                # subfolder, we don't want empty names on folders since they must be
                # unique among siblings
                name = u'{}_{}-{}'.format(self.__class__.__name__,
                                          str(self.id), time.asctime())
            self.folder = repository.root_folder.create_subfolder(name)
            # if not self.group:
            #   self.group = Group(name=self.name)

        if not self.image:
            fn = join(dirname(__file__), "views/data", "community.png")
            self.image = Blob(open(fn, 'rb').read())

    @property
    def has_calendar(self):
        config = current_app.config
        return bool(config.get("ENABLE_CALENDAR"))

    def rename(self, name):
        self.name = name
        if self.folder:
            # FIXME: use signals
            self.folder.name = name

    def get_memberships(self, role=None):
        M = Membership
        memberships = M.query.filter(M.community_id == self.id)
        if role:
            memberships = memberships.filter(M.role == role)
        return memberships.all()

    def set_membership(self, user, role):
        """
        Add a member with the given role, or set the role of an existing
        member.
        """
        assert isinstance(user, User)
        role = Role(role)

        if role not in VALID_ROLES:
            raise ValueError("Invalid role: {}".format(role))

        session = sa.orm.object_session(self) or db.session()
        is_new = True
        M = Membership
        membership = session.query(M) \
            .filter(and_(M.user_id == user.id,
                         M.community_id == self.id)) \
            .first()

        if not membership:
            membership = Membership(community=self, user=user, role=role)
            session.add(membership)
            self.membership_count += 1
        else:
            is_new = False
            membership.role = role

        signals.membership_set.send(self, membership=membership, is_new=is_new)

    def remove_membership(self, user):
        M = Membership
        membership = M.query \
            .filter(and_(M.user_id == user.id, M.community_id == self.id)) \
            .first()
        if not membership:
            raise KeyError("User {} is not a member of community {}".format(
                user, self))

        db.session.delete(membership)
        self.membership_count -= 1
        signals.membership_removed.send(self, membership=membership)

    def update_roles_on_folder(self):
        if self.folder:
            self.ungrant_all_roles_on_folder()
            for membership in self.memberships:
                user = membership.user
                role = membership.role
                if role == MANAGER:
                    security.grant_role(user, MANAGER, self.folder)
                else:
                    if self.type == 'participative':
                        security.grant_role(user, WRITER, self.folder)
                    else:
                        security.grant_role(user, READER, self.folder)

    def ungrant_all_roles_on_folder(self):
        if self.folder:
            role_assignments = security.get_role_assignements(self.folder)
            for principal, role in role_assignments:
                security.ungrant_role(principal, role, self.folder)

    def get_role(self, user):
        """Returns the given user's role in this community.
        """
        M = Membership
        membership = current_app.db.session() \
            .query(M.role) \
            .filter(and_(M.community_id == self.id,
                         M.user_id == user.id)) \
            .first()

        return membership.role if membership else None

    def has_member(self, user):
        return self.get_role(user) is not None

    def has_permission(self, user, permission):
        if not isinstance(permission, Permission):
            assert isinstance(permission, str)
            permission = Permission(permission)

        if user.has_role(Admin):
            return True
        role = self.get_role(user)
        if role == MANAGER:
            return True
        if role == MEMBER and permission in (READ, WRITE):
            return True
        return False

    def touch(self):
        self.last_active_at = datetime.utcnow()

    @property
    def _indexable_roles_and_users(self):
        return indexable_roles_and_users(self)


def CommunityIdColumn():
    return Column(
        ForeignKey(Community.id),
        nullable=False,
        info=SEARCHABLE | dict(index_to=(('community_id', ('community_id',)),)))


# Handlers to keep community/group members in sync
#
_PROCESSED_ATTR = '__sbe_community_group_sync_processed__'


@signals.membership_set.connect_via(ANY)
def _membership_added(sender, membership, is_new):
    if not is_new:
        return

    if getattr(membership.user, _PROCESSED_ATTR, False) is OP_APPEND:
        return

    if sender.group and membership.user not in sender.group.members:
        logger.debug('_membership_added(%r, %r, %r) user: %r', sender,
                     membership, is_new, membership.user)
        setattr(membership.user, _PROCESSED_ATTR, OP_APPEND)
        sender.group.members.add(membership.user)


@signals.membership_removed.connect_via(ANY)
def membership_removed(sender, membership):
    if getattr(membership.user, _PROCESSED_ATTR, False) is OP_REMOVE:
        return

    if sender.group and membership.user in sender.group.members:
        logger.debug('_membership_removed(%r, %r) user: %r', sender, membership,
                     membership.user)
        setattr(membership.user, _PROCESSED_ATTR, OP_REMOVE)
        sender.group.members.discard(membership.user)


@listens_for(Community.members, 'append')
@listens_for(Community.members, 'remove')
def _on_member_change(community, user, initiator):
    group = community.group
    if not group:
        return

    logger.debug('_on_member_change(%r, %r, op=%r)', community, user,
                 initiator.op)

    if getattr(user, _PROCESSED_ATTR, False) is initiator.op:
        return

    setattr(user, _PROCESSED_ATTR, initiator.op)

    if initiator.op is OP_APPEND:
        if user not in group.members:
            group.members.add(user)

    elif initiator.op is OP_REMOVE:
        if user in group.members:
            group.members.discard(user)


@listens_for(Community.group, 'set', active_history=True)
def _on_linked_group_change(community, value, oldvalue, initiator):
    if value == oldvalue:
        return

    logger.debug('_on_linked_group_change(%r, %r, %r)', community, value,
                 oldvalue)

    if oldvalue is not None and oldvalue.members:
        logger.debug('_on_linked_group_change(%r, %r, %r): oldvalue clear()',
                     community, value, oldvalue)
        oldvalue.members.clear()

    members = set(community.members)
    if value is not None and value.members != members:
        logger.debug('_on_linked_group_change(%r, %r, %r): set value.members',
                     community, value, oldvalue)
        value.members = members


def _safe_get_community(group):
    session = sa.orm.object_session(group)
    if not session:
        return None

    with session.no_autoflush:
        try:
            return session.query(Community) \
                .filter(Community.group == group) \
                .options(sa.orm.joinedload(Community.group),
                         sa.orm.joinedload(Community.members)) \
                .one()
        except sa.orm.exc.NoResultFound:
            return None


@listens_for(Group.members, 'append')
@listens_for(Group.members, 'remove')
def _on_group_member_change(group, user, initiator):
    community = _safe_get_community(group)

    if not community:
        return

    op = initiator.op
    if getattr(user, _PROCESSED_ATTR, False) is op:
        return

    is_present = user in community.members
    setattr(user, _PROCESSED_ATTR, op)
    logger.debug('_on_group_member_change(%r, %r, op=%r) community: %r', group,
                 user, initiator.op, community)

    if ((op is OP_APPEND and is_present) or
        (op is OP_REMOVE and not is_present)):
        return

    if op is OP_APPEND:
        community.set_membership(user, MEMBER)

    elif op is OP_REMOVE:
        community.remove_membership(user)


@listens_for(Group.members, 'set', active_history=True)
def _on_group_members_replace(group, value, oldvalue, initiator):
    if value == oldvalue:
        return

    community = _safe_get_community(group)
    if not community:
        return

    members = set(community.members)
    logger.debug('_on_group_members_replace(%r, %r, %r) community: %r', group,
                 value, oldvalue, community)

    for u in members - value:
        if getattr(u, _PROCESSED_ATTR, False) is OP_REMOVE:
            continue
        setattr(u, _PROCESSED_ATTR, OP_REMOVE)
        community.remove_membership(u)

    for u in value - members:
        if getattr(u, _PROCESSED_ATTR, False) is OP_APPEND:
            continue
        setattr(u, _PROCESSED_ATTR, OP_APPEND)
        community.set_membership(u, MEMBER)
