# coding=utf-8
import typing
from datetime import datetime
from enum import Enum

from slugify import slugify
from sqlalchemy.orm import Session
from tracim_backend.config import CFG
from tracim_backend.config import PreviewDim
from tracim_backend.extensions import app_list
from tracim_backend.lib.core.application import ApplicationApi
from tracim_backend.lib.utils.utils import get_root_frontend_url
from tracim_backend.lib.utils.utils import password_generator
from tracim_backend.lib.utils.utils import CONTENT_FRONTEND_URL_SCHEMA
from tracim_backend.lib.utils.utils import WORKSPACE_FRONTEND_URL_SCHEMA
from tracim_backend.models import User
from tracim_backend.models.auth import Profile
from tracim_backend.models.auth import Group
from tracim_backend.models.data import Content
from tracim_backend.models.data import ContentRevisionRO
from tracim_backend.models.data import Workspace
from tracim_backend.models.data import UserRoleInWorkspace
from tracim_backend.models.roles import WorkspaceRoles
from tracim_backend.app_models.workspace_menu_entries import WorkspaceMenuEntry
from tracim_backend.app_models.contents import CONTENT_TYPES


class PreviewAllowedDim(object):

    def __init__(
            self,
            restricted:bool,
            dimensions: typing.List[PreviewDim]
    ) -> None:
        self.restricted = restricted
        self.dimensions = dimensions


class MoveParams(object):
    """
    Json body params for move action model
    """
    def __init__(self, new_parent_id: str, new_workspace_id: str = None) -> None:  # nopep8
        self.new_parent_id = new_parent_id
        self.new_workspace_id = new_workspace_id


class LoginCredentials(object):
    """
    Login credentials model for login model
    """

    def __init__(self, email: str, password: str) -> None:
        self.email = email
        self.password = password


class SetEmail(object):
    """
    Just an email
    """
    def __init__(self, loggedin_user_password: str, email: str) -> None:
        self.loggedin_user_password = loggedin_user_password
        self.email = email


class SetPassword(object):
    """
    Just an password
    """
    def __init__(self,
        loggedin_user_password: str,
        new_password: str,
        new_password2: str
    ) -> None:
        self.loggedin_user_password = loggedin_user_password
        self.new_password = new_password
        self.new_password2 = new_password2


class UserInfos(object):
    """
    Just some user infos
    """
    def __init__(self, timezone: str, public_name: str) -> None:
        self.timezone = timezone
        self.public_name = public_name


class UserProfile(object):
    """
    Just some user infos
    """
    def __init__(self, profile: str) -> None:
        self.profile = profile


class UserCreation(object):
    """
    Just some user infos
    """
    def __init__(
            self,
            email: str,
            password: str = None,
            public_name: str = None,
            timezone: str = None,
            profile: str = None,
            email_notification: bool = True,
    ) -> None:
        self.email = email
        # INFO - G.M - 2018-08-16 - cleartext password, default value
        # is auto-generated.
        self.password = password or password_generator()
        self.public_name = public_name or None
        self.timezone = timezone or ''
        self.profile = profile or Group.TIM_USER_GROUPNAME
        self.email_notification = email_notification


class WorkspaceAndContentPath(object):
    """
    Paths params with workspace id and content_id model
    """
    def __init__(self, workspace_id: int, content_id: int) -> None:
        self.content_id = content_id
        self.workspace_id = workspace_id


class WorkspaceAndContentRevisionPath(object):
    """
    Paths params with workspace id and content_id model
    """
    def __init__(self, workspace_id: int, content_id: int, revision_id) -> None:
        self.content_id = content_id
        self.revision_id = revision_id
        self.workspace_id = workspace_id


class ContentPreviewSizedPath(object):
    """
    Paths params with workspace id and content_id, width, heigth
    """
    def __init__(self, workspace_id: int, content_id: int, width: int, height: int) -> None:  # nopep8
        self.content_id = content_id
        self.workspace_id = workspace_id
        self.width = width
        self.height = height


class RevisionPreviewSizedPath(object):
    """
    Paths params with workspace id and content_id, revision_id width, heigth
    """
    def __init__(self, workspace_id: int, content_id: int, revision_id: int, width: int, height: int) -> None:  # nopep8
        self.content_id = content_id
        self.revision_id = revision_id
        self.workspace_id = workspace_id
        self.width = width
        self.height = height


class WorkspaceAndUserPath(object):
    """
    Paths params with workspace id and user_id
    """
    def __init__(self, workspace_id: int, user_id: int):
        self.workspace_id = workspace_id
        self.user_id = workspace_id


class UserWorkspaceAndContentPath(object):
    """
    Paths params with user_id, workspace id and content_id model
    """
    def __init__(self, user_id: int, workspace_id: int, content_id: int) -> None:  # nopep8
        self.content_id = content_id
        self.workspace_id = workspace_id
        self.user_id = user_id


class CommentPath(object):
    """
    Paths params with workspace id and content_id and comment_id model
    """
    def __init__(
        self,
        workspace_id: int,
        content_id: int,
        comment_id: int
    ) -> None:
        self.content_id = content_id
        self.workspace_id = workspace_id
        self.comment_id = comment_id


class AutocompleteQuery(object):
    """
    Autocomplete query model
    """
    def __init__(self, acp: str):
        self.acp = acp


class PageQuery(object):
    """
    Page query model
    """
    def __init__(
            self,
            page: int = 0
    ):
        self.page = page


class ContentFilter(object):
    """
    Content filter model
    """
    def __init__(
            self,
            workspace_id: int = None,
            parent_id: int = None,
            show_archived: int = 0,
            show_deleted: int = 0,
            show_active: int = 1,
            content_type: str = None,
            offset: int = None,
            limit: int = None,
    ) -> None:
        self.parent_id = parent_id
        self.workspace_id = workspace_id
        self.show_archived = bool(show_archived)
        self.show_deleted = bool(show_deleted)
        self.show_active = bool(show_active)
        self.limit = limit
        self.offset = offset
        self.content_type = content_type


class ActiveContentFilter(object):
    def __init__(
            self,
            limit: int = None,
            before_content_id: datetime = None,
    ):
        self.limit = limit
        self.before_content_id = before_content_id


class ContentIdsQuery(object):
    def __init__(
            self,
            contents_ids: typing.List[int] = None,
    ):
        self.contents_ids = contents_ids


class RoleUpdate(object):
    """
    Update role
    """
    def __init__(
        self,
        role: str,
        do_notify: bool,
    ):
        self.role = role
        self.do_notify = do_notify


class WorkspaceMemberInvitation(object):
    """
    Workspace Member Invitation
    """
    def __init__(
        self,
        user_id: int,
        user_email_or_public_name: str,
        role: str,
        do_notify: str,
    ):
        self.role = role
        self.user_email_or_public_name = user_email_or_public_name
        self.user_id = user_id
        self.do_notify = do_notify


class WorkspaceUpdate(object):
    """
    Update workspace
    """
    def __init__(
        self,
        label: str,
        description: str,
    ):
        self.label = label
        self.description = description


class ContentCreation(object):
    """
    Content creation model
    """
    def __init__(
        self,
        label: str,
        content_type: str,
        parent_id: typing.Optional[int] = None,
    ) -> None:
        self.label = label
        self.content_type = content_type
        self.parent_id = parent_id or None


class CommentCreation(object):
    """
    Comment creation model
    """
    def __init__(
        self,
        raw_content: str,
    ) -> None:
        self.raw_content = raw_content


class SetContentStatus(object):
    """
    Set content status
    """
    def __init__(
        self,
        status: str,
    ) -> None:
        self.status = status


class TextBasedContentUpdate(object):
    """
    TextBasedContent update model
    """
    def __init__(
        self,
        label: str,
        raw_content: str,
    ) -> None:
        self.label = label
        self.raw_content = raw_content


class FolderContentUpdate(object):
    """
    Folder Content update model
    """
    def __init__(
        self,
        label: str,
        raw_content: str,
        sub_content_types: typing.List[str],
    ) -> None:
        self.label = label
        self.raw_content = raw_content
        self.sub_content_types = sub_content_types


class TypeUser(Enum):
    """Params used to find user"""
    USER_ID = 'found_id'
    EMAIL = 'found_email'
    PUBLIC_NAME = 'found_public_name'


class UserInContext(object):
    """
    Interface to get User data and User data related to context.
    """

    def __init__(self, user: User, dbsession: Session, config: CFG):
        self.user = user
        self.dbsession = dbsession
        self.config = config

    # Default

    @property
    def email(self) -> str:
        return self.user.email

    @property
    def user_id(self) -> int:
        return self.user.user_id

    @property
    def public_name(self) -> str:
        return self.display_name

    @property
    def display_name(self) -> str:
        return self.user.display_name

    @property
    def created(self) -> datetime:
        return self.user.created

    @property
    def is_active(self) -> bool:
        return self.user.is_active

    @property
    def timezone(self) -> str:
        return self.user.timezone

    @property
    def profile(self) -> Profile:
        return self.user.profile.name

    @property
    def is_deleted(self) -> bool:
        return self.user.is_deleted

    # Context related

    @property
    def calendar_url(self) -> typing.Optional[str]:
        # TODO - G-M - 20-04-2018 - [Calendar] Replace calendar code to get
        # url calendar url.
        #
        # from tracim.lib.calendar import CalendarManager
        # calendar_manager = CalendarManager(None)
        # return calendar_manager.get_workspace_calendar_url(self.workspace_id)
        return None

    @property
    def avatar_url(self) -> typing.Optional[str]:
        # TODO - G-M - 20-04-2018 - [Avatar] Add user avatar feature
        return None


class WorkspaceInContext(object):
    """
    Interface to get Workspace data and Workspace data related to context.
    """

    def __init__(self, workspace: Workspace, dbsession: Session, config: CFG):
        self.workspace = workspace
        self.dbsession = dbsession
        self.config = config

    @property
    def workspace_id(self) -> int:
        """
        numeric id of the workspace.
        """
        return self.workspace.workspace_id

    @property
    def id(self) -> int:
        """
        alias of workspace_id
        """
        return self.workspace_id

    @property
    def label(self) -> str:
        """
        get workspace label
        """
        return self.workspace.label

    @property
    def description(self) -> str:
        """
        get workspace description
        """
        return self.workspace.description

    @property
    def slug(self) -> str:
        """
        get workspace slug
        """
        return slugify(self.workspace.label)

    @property
    def is_deleted(self) -> bool:
        """
        Is the workspace deleted ?
        """
        return self.workspace.is_deleted

    @property
    def sidebar_entries(self) -> typing.List[WorkspaceMenuEntry]:
        """
        get sidebar entries, those depends on activated apps.
        """
        # TODO - G.M - 22-05-2018 - Rework on this in
        # order to not use hardcoded list
        # list should be able to change (depending on activated/disabled
        # apps)
        app_api = ApplicationApi(
            app_list
        )
        return app_api.get_default_workspace_menu_entry(self.workspace)

    @property
    def frontend_url(self):
        root_frontend_url = get_root_frontend_url(self.config)
        workspace_frontend_url = WORKSPACE_FRONTEND_URL_SCHEMA.format(
            workspace_id=self.workspace_id,
        )
        return root_frontend_url + workspace_frontend_url


class UserRoleWorkspaceInContext(object):
    """
    Interface to get UserRoleInWorkspace data and related content

    """
    def __init__(
            self,
            user_role: UserRoleInWorkspace,
            dbsession: Session,
            config: CFG,
            # Extended params
            newly_created: bool = None,
            email_sent: bool = None
    )-> None:
        self.user_role = user_role
        self.dbsession = dbsession
        self.config = config
        # Extended params
        self.newly_created = newly_created
        self.email_sent = email_sent

    @property
    def user_id(self) -> int:
        """
        User who has the role has this id
        :return: user id as integer
        """
        return self.user_role.user_id

    @property
    def workspace_id(self) -> int:
        """
        This role apply only on the workspace with this workspace_id
        :return: workspace id as integer
        """
        return self.user_role.workspace_id

    # TODO - G.M - 23-05-2018 - Check the API spec for this this !

    @property
    def role_id(self) -> int:
        """
        role as int id, each value refer to a different role.
        """
        return self.user_role.role

    @property
    def role(self) -> str:
        return self.role_slug

    @property
    def role_slug(self) -> str:
        """
        simple name of the role of the user.
        can be anything from UserRoleInWorkspace SLUG, like
        'not_applicable', 'reader',
        'contributor', 'content-manager', 'workspace-manager'
        :return: user workspace role as slug.
        """
        return WorkspaceRoles.get_role_from_level(self.user_role.role).slug

    @property
    def is_active(self) -> bool:
        return self.user.is_active

    @property
    def do_notify(self) -> bool:
        return self.user_role.do_notify

    @property
    def user(self) -> UserInContext:
        """
        User who has this role, with context data
        :return: UserInContext object
        """
        return UserInContext(
            self.user_role.user,
            self.dbsession,
            self.config
        )

    @property
    def workspace(self) -> WorkspaceInContext:
        """
        Workspace related to this role, with his context data
        :return: WorkspaceInContext object
        """
        return WorkspaceInContext(
            self.user_role.workspace,
            self.dbsession,
            self.config
        )


class ContentInContext(object):
    """
    Interface to get Content data and Content data related to context.
    """

    def __init__(self, content: Content, dbsession: Session, config: CFG, user: User=None):  # nopep8
        self.content = content
        self.dbsession = dbsession
        self.config = config
        self._user = user

    # Default
    @property
    def content_id(self) -> int:
        return self.content.content_id

    @property
    def parent_id(self) -> int:
        """
        Return parent_id of the content
        """
        return self.content.parent_id

    @property
    def workspace_id(self) -> int:
        return self.content.workspace_id

    @property
    def label(self) -> str:
        return self.content.label

    @property
    def content_type(self) -> str:
        content_type = CONTENT_TYPES.get_one_by_slug(self.content.type)
        return content_type.slug

    @property
    def sub_content_types(self) -> typing.List[str]:
        return [_type.slug for _type in self.content.get_allowed_content_types()]  # nopep8

    @property
    def status(self) -> str:
        return self.content.status

    @property
    def is_archived(self):
        return self.content.is_archived

    @property
    def is_deleted(self):
        return self.content.is_deleted

    @property
    def raw_content(self):
        return self.content.description

    @property
    def author(self):
        return UserInContext(
            dbsession=self.dbsession,
            config=self.config,
            user=self.content.first_revision.owner
        )

    @property
    def current_revision_id(self):
        return self.content.revision_id

    @property
    def created(self):
        return self.content.created

    @property
    def modified(self):
        return self.updated

    @property
    def updated(self):
        return self.content.updated

    @property
    def last_modifier(self):
        return UserInContext(
            dbsession=self.dbsession,
            config=self.config,
            user=self.content.last_revision.owner
        )

    # Context-related
    @property
    def show_in_ui(self):
        # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
        # if false, then do not show content in the treeview.
        # This may his maybe used for specific contents or for sub-contents.
        # Default is True.
        # In first version of the API, this field is always True
        return True

    @property
    def slug(self):
        return slugify(self.content.label)

    @property
    def read_by_user(self):
        assert self._user
        return not self.content.has_new_information_for(self._user)

    @property
    def frontend_url(self):
        root_frontend_url = get_root_frontend_url(self.config)
        content_frontend_url = CONTENT_FRONTEND_URL_SCHEMA.format(
            workspace_id=self.workspace_id,
            content_type=self.content_type,
            content_id=self.content_id,
        )
        return root_frontend_url + content_frontend_url


class RevisionInContext(object):
    """
    Interface to get Content data and Content data related to context.
    """

    def __init__(self, content_revision: ContentRevisionRO, dbsession: Session, config: CFG):
        assert content_revision is not None
        self.revision = content_revision
        self.dbsession = dbsession
        self.config = config

    # Default
    @property
    def content_id(self) -> int:
        return self.revision.content_id

    @property
    def parent_id(self) -> int:
        """
        Return parent_id of the content
        """
        return self.revision.parent_id

    @property
    def workspace_id(self) -> int:
        return self.revision.workspace_id

    @property
    def label(self) -> str:
        return self.revision.label

    @property
    def revision_type(self) -> str:
        return self.revision.revision_type

    @property
    def content_type(self) -> str:
        return CONTENT_TYPES.get_one_by_slug(self.revision.type).slug

    @property
    def sub_content_types(self) -> typing.List[str]:
        return [_type.slug for _type
                in self.revision.node.get_allowed_content_types()]

    @property
    def status(self) -> str:
        return self.revision.status

    @property
    def is_archived(self) -> bool:
        return self.revision.is_archived

    @property
    def is_deleted(self) -> bool:
        return self.revision.is_deleted

    @property
    def raw_content(self) -> str:
        return self.revision.description

    @property
    def author(self) -> UserInContext:
        return UserInContext(
            dbsession=self.dbsession,
            config=self.config,
            user=self.revision.owner
        )

    @property
    def revision_id(self) -> int:
        return self.revision.revision_id

    @property
    def created(self) -> datetime:
        return self.updated

    @property
    def modified(self) -> datetime:
        return self.updated

    @property
    def updated(self) -> datetime:
        return self.revision.updated

    @property
    def next_revision(self) -> typing.Optional[ContentRevisionRO]:
        """
        Get next revision (later revision)
        :return: next_revision
        """
        next_revision = None
        revisions = self.revision.node.revisions
        # INFO - G.M - 2018-06-177 - Get revisions more recent that
        # current one
        next_revisions = [
            revision for revision in revisions
            if revision.revision_id > self.revision.revision_id
        ]
        if next_revisions:
            # INFO - G.M - 2018-06-177 -sort revisions by date
            sorted_next_revisions = sorted(
                next_revisions,
                key=lambda revision: revision.updated
            )
            # INFO - G.M - 2018-06-177 - return only next revision
            return sorted_next_revisions[0]
        else:
            return None

    @property
    def comment_ids(self) -> typing.List[int]:
        """
        Get list of ids of all current revision related comments
        :return: list of comments ids
        """
        comments = self.revision.node.get_comments()
        # INFO - G.M - 2018-06-177 - Get comments more recent than revision.
        revision_comments = [
            comment for comment in comments
            if comment.created > self.revision.updated
        ]
        if self.next_revision:
            # INFO - G.M - 2018-06-177 - if there is a revision more recent
            # than current remove comments from theses rev (comments older
            # than next_revision.)
            revision_comments = [
                comment for comment in revision_comments
                if comment.created < self.next_revision.updated
            ]
        sorted_revision_comments = sorted(
            revision_comments,
            key=lambda revision: revision.created
        )
        comment_ids = []
        for comment in sorted_revision_comments:
            comment_ids.append(comment.content_id)
        return comment_ids

    # Context-related
    @property
    def show_in_ui(self) -> bool:
        # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
        # if false, then do not show content in the treeview.
        # This may his maybe used for specific contents or for sub-contents.
        # Default is True.
        # In first version of the API, this field is always True
        return True

    @property
    def slug(self) -> str:
        return slugify(self.revision.label)
