from collections import defaultdict
from collections import OrderedDict
from logging import getLogger
from plone import api
from plone.memoize import forever
from plone.memoize.view import memoize
from plone.memoize.view import memoize_contextless
from plone.protect.authenticator import createToken
from ploneintranet.core import _
from ploneintranet.layout.interfaces import IAppView
from ploneintranet.search.interfaces import ISiteSearch
from ploneintranet.todo.interfaces import IMilestoneNameResolver
from ploneintranet.todo.utils import get_review_state_hr
from ploneintranet.todo.vocabularies import todo_priority
from ploneintranet.workspace.interfaces import IMetroMap
from ploneintranet.workspace.utils import parent_workspace
from Products.Five import BrowserView
from scorched.dates import solr_date
from urllib.parse import parse_qsl
from urllib.parse import urlencode
from zope.component import getUtility
from zope.interface import implementer

logger = getLogger(__name__)


class InvalidFiltersException(ValueError):
    """Raise this if the search filters are invalid"""


@implementer(IAppView)
class View(BrowserView):
    """The view for this app"""

    app_name = "todo"
    title = _("label_title_app_todo", default="Todo")

    _done_states = ("done",)

    _navigation_tabs = OrderedDict(
        [
            ("@@app-todo", _("All tasks")),
            ("@@my-tasks", _("My tasks")),
            ("@@personal-tasks", _("Personal tasks")),
        ]
    )

    _browse_mode_default = "origin"
    _browse_mode_options = OrderedDict(
        [
            ("origin", _("Group by origin")),
            ("origin_and_milestone", _("Group by origin and milestone")),
            ("assignee", _("Group by assignee")),
            ("initiator", _("Group by initiator")),
            ("task_state", _("Group by task state")),
            ("priority", _("Group by priority")),
            ("", _("No grouping")),
        ]
    )

    _sort_mode_default = "-modified"
    _sort_mode_options = OrderedDict(
        [
            ("-modified", _("Newest first")),
            ("modified", _("Oldest first")),
            ("assignee", _("By assignee")),
            ("initiator", _("By initiator")),
            ("review_state", _("By task state")),
            ("-priority", _("By priority")),
            ("sortable_title", _("Alphabetically")),
        ]
    )

    _state_mode_default = ""
    _state_mode_options = OrderedDict(
        [("open", _("Open tasks")), ("done", _("Closed tasks")), ("", _("All tasks"))]
    )

    _search_tasks_limit = 100
    _cookie_name = "app-todo"
    _ignored__form_keys = {"_", "_authenticator", "app", "reset-filters"}

    show_initiators = True
    show_assignes = True

    dpi_on_toggle_task_state = (
        "source: #document-body; target: #document-body;  && "
        "source: #global-statusmessage; target: #global-statusmessage;"
    )
    todo_link_suffix = "app"

    def get_cookie_params(self):
        """Get the params stored in the cookie unless explicitly requested"""
        if not self._cookie_name:
            return {}
        if "reset-filters" in self.request.form:
            return {}
        cookie = self.request.cookies.get(self._cookie_name)
        try:
            params = dict(parse_qsl(cookie))
        except AttributeError:
            params = {}
        return params

    def set_cookie_params(self, params):
        """Set the current search parameters into a cookie"""
        if not self._cookie_name:
            return None
        if not params and "reset-filters" in self.request.form:
            self.request.response.setCookie(
                self._cookie_name, "", path="/", expires="Thu, 01 Jan 1970 00:00:00 UTC"
            )
        self.request.response.setCookie(
            self._cookie_name,
            urlencode(
                [
                    (key, params[key])
                    for key in sorted(params)
                    if params[key] and key not in self._ignored__form_keys
                ]
            ),
            path="/",
        )

    @property
    @memoize_contextless
    def form_params(self):
        """Persist in a cookie the search options"""
        params = self.get_cookie_params()
        for key, value in self.request.form.items():
            if key not in self._ignored__form_keys:
                if value:
                    params[key] = value
                else:
                    params.pop(key, None)
        self.set_cookie_params(params)
        return params

    @property
    @memoize_contextless
    def allowed_columns(self):
        """List the columns we should display in the results

        This depends on the grouping
        """
        all_columns = {"assignee", "due", "initiator", "milestone"}
        browse_mode = self.browse_mode
        if browse_mode == "assignee":
            all_columns.remove("assignee")
        elif browse_mode == "initiator":
            all_columns.remove("initiator")
        else:
            all_columns.remove("milestone")
        return all_columns

    @property
    @memoize_contextless
    def portal(self):
        """Return the userprofile container view"""
        return api.portal.get()

    @property
    @memoize_contextless
    def userprofiles(self):
        """Return the userprofile container view"""
        return api.content.get_view("view", self.portal.profiles, self.request)

    @property
    def results_view(self):
        """Return a view that renders the search results"""
        return api.content.get_view(
            f"{self.__name__}-search-results", self.context, self.request
        )

    def reset_url(self):
        """Return the url without -search-results"""
        name = self.__name__
        if name.endswith("-search-results"):
            name = name.rsplit("-", 2)[0]
        return name

    @memoize_contextless
    def get_prio_class(self, priority):
        """Return a class to set the priority marker"""
        classes = {0: "priority-low", 1: "priority-medium", 2: "priority-high"}
        return classes.get(priority, "")

    @property
    @memoize
    def allusers_json_url(self):
        """Return @@allusers.json in the proper context"""
        return f"{self.portal.profiles.absolute_url()}/@@allusers.json"

    def get_data_pat_autosuggest(self):
        """Return the data-pat-autosuggest for a fieldname"""
        options = [
            "ajax-data-type: json",
            "maximum-selection-size: 1",
            "selection-classes: {}",
            f"ajax-url: {self.allusers_json_url}",
            "allow-new-words: false",
        ]
        return "; ".join(options)

    @property
    @memoize_contextless
    def workspace_container(self):
        """Return the userprofile container view"""
        return self.portal.get("workspaces", {})

    @property
    @memoize
    def user(self):
        """Get the current user"""
        return self.userprofiles.user

    @property
    @memoize
    def add_task_url(self):
        """The URL for adding a personal tasks"""
        return f"{self.context.absolute_url()}/@@add_task"

    @property
    @memoize
    def navigation_tabs(self):
        """Convenience method to easily render the tabs in the template"""
        base_url = self.context.absolute_url()
        items = [
            {
                "url": f"{base_url}/{view}",
                "label": label,
                "klass": "current" if self.__name__ == view.lstrip("@") else "",
            }
            for view, label in self._navigation_tabs.items()
        ]
        return items

    def options2items(self, options, fieldname, default=""):
        """Make items that will be rendered in a page template"""
        selected = self.form_params.get(fieldname, default)
        return [
            {
                "value": value,
                "label": label,
                "selected": selected == value and "selected" or None,
            }
            for value, label in options.items()
        ]

    @property
    @memoize
    def browse_mode_options(self):
        """The options for the browsing mode"""
        options = self.options2items(
            self._browse_mode_options, "browse-mode", self._browse_mode_default
        )
        return options

    @property
    @memoize
    def browse_mode(self):
        """Look in the stored form params (which can contain values from a
        cookie) and the in the request.
        Fall back to self._browse_mode_default
        """
        return self.form_params.get(
            "browse-mode", self.request.get("browse-mode", self._browse_mode_default)
        )

    @property
    @memoize
    def sort_mode_options(self):
        """The options for the sort mode"""
        options = self.options2items(
            self._sort_mode_options, "sort-mode", self._sort_mode_default
        )
        return options

    @property
    @memoize
    def state_mode_options(self):
        """The options for the sort mode"""
        options = self.options2items(
            self._state_mode_options, "state-mode", self._state_mode_default
        )
        return options

    @property
    @memoize
    def quick_functions(self):
        """Return a list of actions to be rendered in the top os the sidebar"""
        actions = [
            {
                "cssclass": "create icon pat-modal",
                "url": f"{self.add_task_url}#document-content",
                "title": _("Create task"),
                "dpm": "class: panel medium",
            },
            {
                "cssclass": "resize-full icon pat-toggle",
                "url": None,
                "title": _("Expand/reduce sidebar"),
                "dpt": "selector: #application-body; value: sidebar-large sidebar-normal",  # noqa: E501
            },
        ]
        return actions

    @memoize
    def is_done(self, task):
        """Check if the task is done"""
        return api.content.get_state(obj=task) in self._done_states

    def set_filters(self, filters):
        """Customizable filters for the view"""
        form = self.form_params
        filters["portal_type"] = "todo"
        filters["path"] = self.searched_paths
        state_mode = form.get("state-mode", self._state_mode_default)
        if state_mode:
            filters["review_state"] = state_mode
        start = form.get("start", None)
        end = form.get("end", None)
        if start and end:
            if start > end:
                raise InvalidFiltersException(f"Invalid date range: {start!r}, {end!r}")
            filters["due__range"] = (solr_date(start), solr_date(end))
        elif start:
            filters["due__gt"] = solr_date(start)
        elif end:
            filters["due__lt"] = solr_date(end)
        for key in ("assignee", "initiator"):
            value = form.get(key, None)
            if value:
                filters[key] = value

    def search_tasks(self, filters={}, **params):
        """
        Search for specific content types
        """
        filters = filters.copy()
        form = self.form_params
        keywords = form.get("SearchableText")
        try:
            self.set_filters(filters)
        except InvalidFiltersException:
            return []
        search_util = getUtility(ISiteSearch)
        _params = {
            "sort": form.get("sort-mode", self._sort_mode_default),
            "step": self._search_tasks_limit,
        }
        _params.update(params)
        response = search_util.query(keywords, filters, **_params)
        return response

    @memoize
    def show_lenght_warning(self):
        """The search results are limited. Show a warning
        the suggests to play with filters
        """
        return len(self.tasks) >= self._search_tasks_limit

    @property
    @memoize
    def searched_paths(self):
        """Return the tasks inside workspaces"""
        paths = [
            "/".join(obj.getPhysicalPath())
            for obj in (self.workspace_container, self.portal.profiles)
            if obj
        ]
        return paths

    @property
    @memoize
    def tasks(self):
        """Return the tasks inside workspaces"""
        if not self.searched_paths:
            return []
        return [_f for _f in (t.getObject() for t in self.search_tasks()) if _f]

    @property
    @memoize
    def tasks_by_workspace(self):
        """Return the tasks inside workspaces grouped by workspace"""
        tasks = self.tasks
        tasks_by_workspace = defaultdict(list)
        for task in tasks:
            workspace = parent_workspace(task)
            tasks_by_workspace[workspace].append(task)
        return tasks_by_workspace

    def get_key_maker(self):
        """Return a function that will generate a key
        according to the selected browsing mode
        The key can be the parent workspace, an attribute value,
        the review state or whatever.

        As a fallback we return a function that returns None.
        """
        browse_mode = self.browse_mode
        if browse_mode in ("origin", "assigned", "initiated"):
            return parent_workspace
        if browse_mode == "origin_and_milestone":
            return lambda task: (parent_workspace(task), task.milestone)
        if browse_mode == "task_state":
            return api.content.get_state
        if browse_mode == "priority":
            return lambda task: getattr(task, browse_mode, -1)
        if browse_mode in ("assignee", "initiator"):
            return lambda task: getattr(task, browse_mode, "") or ""
        return lambda task: None

    @memoize
    def get_ws_workflow(self, ws):
        """Return the workflow for this workspace"""
        pw = api.portal.get_tool("portal_workflow")
        for wf in pw.getWorkflowsFor(ws):
            return wf

    @memoize
    def get_review_state_hr(self, wf, review_state):
        """Given a workflow and a review state, return a human readable title"""
        return get_review_state_hr(wf, review_state)

    @memoize
    def get_ws_metromap(self, ws):
        """Return the @@metromap view for a workspace (if any)"""
        if not ws:
            return
        try:
            return api.content.get_view("metromap", ws, self.request)
        except api.exc.InvalidParameterError:
            pass

    @memoize
    def get_milestone_hr(self, ws, milestone):
        """Gets the milestone for this workspace human readable

        Tipycally get the ws workflow and look for an homonymous state
        """
        if not (ws and milestone):
            return ""
        adapter = IMilestoneNameResolver(ws, None)
        if adapter:
            name = adapter.resolve(milestone)
            if name:
                return name
        wf = self.get_ws_workflow(ws)
        return self.get_review_state_hr(wf, milestone)

    def get_milestone_idx(self, ws, milestone):
        """Check the metromap index for a given milestone

        It may return 999 when we do not have a milestone set,
        we do not have a ws (personal tasks) or for some reason the milestone
        is not found in the metromap
        """
        mm = self.get_ws_metromap(ws)
        if mm and milestone:
            idx = mm.get_milestone_idx(milestone)
            if idx >= 0:
                return idx
        return 999

    def get_item_milestone_hr(self, item):
        """Given an item, try to get the milestone in a human readable form"""
        milestone = getattr(item, "milestone", None)
        if not milestone:
            return ""
        ws = parent_workspace(item)
        return self.get_milestone_hr(ws, milestone)

    def _origin_key_beautifier(self, key):
        """We expect key to be either a workspace or None"""
        if key:
            return {"key": key, "title": key.title}
        return {
            "icon": "icon-user",
            "klass": "personal",
            "key": key,
            "title": _("Personal tasks"),
            "sorting_key": " ",
        }

    def _origin_and_milestone_key_beautifier(self, key):
        """We expect key to be a tuple containing a workspace and a milestone"""
        ws, milestone = key
        key_beautified = self._origin_key_beautifier(ws)
        key_beautified["cssid"] = f"milestone-{milestone}"
        key_beautified["key"] = key
        key_beautified["header_obj"] = ws
        key_beautified["sorting_key"] = (
            ws and ws.title,
            self.get_milestone_idx(ws, milestone),
        )
        if milestone:
            key_beautified["icon"] = "icon-stage"
            key_beautified["title"] = self.get_milestone_hr(ws, milestone)
        else:
            key_beautified["icon"] = ""
            key_beautified["title"] = _("No milestone set")
        return key_beautified

    def _userid_key_beautifier(self, key):
        """We expect key to be a userid"""
        if key:
            fullname = self.userprofiles.get_fullname_for(key)
        else:
            fullname = ""
        return {
            "title": fullname or _("None"),
            "key": key,
            "sorting_key": fullname or " ",
        }

    def _priority_key_beautifier(self, key):
        """We expect key to be a priority level"""
        try:
            title = todo_priority.getTerm(key).title
        except BaseException:
            logger.warning("Cannot get priority label for value: %r", key)
            title = key
        return {"title": title, "key": key, "sorting_key": str(-key + 50)}

    def _task_state_key_beautifier(self, key):
        """key should be a todo review state."""
        pw = api.portal.get_tool("portal_workflow")
        wf_id = pw.getChainForPortalType("todo")[0]
        wf = pw.getWorkflowById(wf_id)
        title = self.get_review_state_hr(wf, key)
        return {"title": title, "key": key}

    def get_key_beautifier(self):
        """Return a function that will transform a key in to something
        that can be rendered in the template
        """
        browse_mode = self.browse_mode
        if browse_mode in ("origin", "assigned", "initiated"):
            return self._origin_key_beautifier
        if browse_mode == "origin_and_milestone":
            return self._origin_and_milestone_key_beautifier
        if browse_mode in ("assignee", "initiator"):
            return self._userid_key_beautifier
        if browse_mode == "priority":
            return self._priority_key_beautifier
        if browse_mode == "task_state":
            return self._task_state_key_beautifier
        return lambda key: {"title": key, "key": key}

    @property
    @memoize
    def tasks_by_group(self):
        """Return the tasks grouped according to the browse mode

        The beautifier function is a dict like object with a key, a title
        and other optional key/value pairs that may be used in the template
        """
        key_maker = self.get_key_maker()
        tasks_by_group = defaultdict(list)
        for task in self.tasks:
            key = key_maker(task)
            tasks_by_group[key].append(task)
        return tasks_by_group

    @property
    @memoize
    def beautified_keys(self):
        """Return the sorted keys for our browse mode
        The keys need to be beautified in order to be rendered in the template.
        """
        key_beautifier = self.get_key_beautifier()
        groups = map(key_beautifier, self.tasks_by_group)
        return sorted(groups, key=lambda g: g.get("sorting_key") or g["title"])

    def get_actions_for(self, beautified_key):
        """This will return some actions to be executed
        in the context of the grouping beuatified keys
        """
        actions = []
        key = beautified_key["key"]
        if isinstance(key, tuple):
            target, milestone = key
        else:
            target = key
            milestone = ""
        if isinstance(target, (int, (str,))):
            return actions
        if not target:
            if not self.user:
                return actions
            else:
                target = self.user

        if api.user.has_permission(
            user=self.user, obj=target, permission="Add portal content"
        ):
            actions.append(
                {
                    "cssclass": "icon-plus-circle pat-button pat-modal",
                    "dpm": "class: large panel;",
                    "label": _("Create new task"),
                    "url": "{url}?{qs}{fragment}".format(
                        url=self.add_task_url,
                        qs=urlencode(
                            {
                                "container": "/".join(target.getPhysicalPath()),
                                "milestone": milestone,
                            }
                        ),
                        fragment="#document-content",
                    ),
                }
            )
        return actions

    def get_fieldset_header(self, beautified_key, seen):
        """We may have objects grouped by two criteria,
        e.g. workspace and milestone.
        In that case abowe each group we want to add an additional header,
        but just once.
        To do that we look for an header_obj in the key.
        If found and not present in the seen object (which should be listish),
        We return an appropriate string.
        """
        obj = beautified_key.get("header_obj", None)
        if not obj or obj in seen:
            return ""
        seen.append(obj)
        return obj.title

    @memoize
    def show_no_results(self):
        """Check if we should show the no result notice"""
        return not self.tasks

    def show_reset_button(self):
        """Show the reset button if we have something in the query string"""
        keys = set(self.form_params)
        return keys - self._ignored__form_keys


class MyTasksView(View):
    """The tasks assigned to me"""

    show_assignes = False

    @property
    @forever.memoize
    def _browse_mode_options(self):
        """Remove the assignee option because it is always the current user"""
        options = super()._browse_mode_options.copy()
        options.pop("assignee")
        return options

    def set_filters(self, filters={}, **params):
        """Filter by assignee with my userid"""
        super().set_filters(filters)
        if not self.user:
            return
        filters["assignee"] = self.user.username


class PersonalTasksView(View):
    """My personal tasks"""

    _browse_mode_default = ""
    _browse_mode_options = OrderedDict(
        [
            ("", _("All personal tasks")),
            ("assigned", _("Personal tasks assigned to me by others")),
            ("initiated", _("Personal tasks that I assigned to others")),
        ]
    )

    show_initiators = False
    show_assignes = False

    def set_filters(self, filters={}, **params):
        """Filter by assignee with my userid"""
        super().set_filters(filters)
        browse_mode = self.browse_mode
        if browse_mode == "assigned":
            filters["assignee"] = self.user.username
        elif browse_mode == "initiated":
            filters["initiator"] = self.user.username

    def get_key_maker(self):
        """Personal tasks are not grouped"""
        return lambda task: None

    def get_key_beautifier(self):
        """Return a function that will transform a key in to something
        that can be rendered in the template
        """
        return self._origin_key_beautifier

    @property
    @memoize
    def searched_paths(self):
        """Return the tasks inside workspaces"""
        if not self.user and self.browse_mode:
            return []
        return ["/".join(self.portal.profiles.getPhysicalPath())]


class WorkspaceTasksView(View):
    _browse_mode_default = ""
    _cookie_name = ""
    navigation_tabs = []
    todo_link_suffix = ""

    def show_reset_button(self):
        """Never show the reset button in the workspaces sidebar"""
        return False

    @property
    @forever.memoize
    def _browse_mode_options(self):
        """
        Custom groupings for the todo sidebar options
        """
        options = super()._browse_mode_options.copy()
        options.pop("origin")
        options.pop("origin_and_milestone")
        return options

    def get_actions_for(self, beautified_key):
        """Check if we can append the workflow action"""
        if not self.browse_mode:
            # When there is no grouping do not display the add action
            return []
        return super().get_actions_for(beautified_key)

    @property
    @memoize
    def searched_paths(self):
        """Return the tasks inside workspaces"""
        return ["/".join(parent_workspace(self.context).getPhysicalPath())]

    def get_fieldset_header(self, beautified_key, seen):
        """We may have objects grouped by two criteria,
        e.g. workspace and milestone.
        In that case abowe each group we want to add an additional header,
        but just once.
        To do that we look for an header_obj in the key.
        If found and not present in the seen object (which should be listish),
        We return an appropriate string.
        """
        if beautified_key.get("title", None) is None:
            return _("General tasks")
        return super().get_fieldset_header(beautified_key, seen)


class CaseTasksView(WorkspaceTasksView):
    _browse_mode_default = "origin_and_milestone"
    dpi_on_toggle_task_state = (
        "source: #document-body; target: #document-body; && "
        "source: #sidebar-content-injection; target: #sidebar-content-injection; && "
        "source: #global-statusmessage; target: #global-statusmessage;"
    )

    @property
    @forever.memoize
    def _browse_mode_options(self):
        """
        Custom groupings for the todo sidebar options
        """
        options = super(WorkspaceTasksView, self)._browse_mode_options.copy()
        options.pop("origin")
        options["origin_and_milestone"] = _("Group by milestone")
        return options

    @property
    @memoize
    def metromap_sequence(self):
        return IMetroMap(self.context).metromap_sequence

    def get_actions_for(self, beautified_key):
        """Check if we can append the workflow action"""
        actions = super().get_actions_for(beautified_key)
        if self.browse_mode != "origin_and_milestone":
            return actions

        params = {"_authenticator": createToken()}
        ws, milestone = beautified_key["key"]
        metromap_sequence = self.metromap_sequence
        transitions = metromap_sequence.get(milestone, {})

        if not (
            transitions.get("transition_enabled")
            or transitions.get("reopen_transition")
        ):
            return actions

        wf_action_template = "{base_url}/content_status_modify?{qs}"
        wf_action = {
            "cssclass": "small icon-stage success button pat-inject",
            "dpi": "source: #sidebar-content-injection; target: #sidebar-content-injection;",  # noqa: E501
        }

        if transitions.get("transition_enabled"):
            params["workflow_action"] = transitions["transition_id"]
            wf_action.update(
                {
                    "label": _("Close"),
                    "url": wf_action_template.format(
                        base_url=self.context.absolute_url(), qs=urlencode(params)
                    ),
                }
            )
        elif transitions.get("reopen_transition"):
            params["workflow_action"] = transitions["reopen_transition"]
            wf_action.update(
                {
                    "label": _("Reopen"),
                    "url": wf_action_template.format(
                        base_url=self.context.absolute_url(), qs=urlencode(params)
                    ),
                }
            )

        actions.append(wf_action)

        return actions

    @property
    @memoize
    def beautified_keys(self):
        """Return the sorted keys for our browse mode
        The keys need to be beautified in order to be rendered in the template.
        """
        if not self.browse_mode or self.browse_mode != "origin_and_milestone":
            return super().beautified_keys
        key_beautifier = self.get_key_beautifier()
        unbeautified_keys = [(self.context, state) for state in self.metromap_sequence]
        for additional_key in self.tasks_by_group:
            if additional_key not in unbeautified_keys:
                unbeautified_keys.append(additional_key)
        beautified_keys = map(key_beautifier, unbeautified_keys)
        return list(beautified_keys)
