from collections import OrderedDict
from DateTime import DateTime
from logging import getLogger
from plone import api
from plone.app.blocks.interfaces import IBlocksTransformEnabled
from plone.app.contenttypes.interfaces import IDocument
from plone.app.contenttypes.interfaces import IFile
from plone.app.contenttypes.interfaces import IImage
from plone.dexterity.utils import datify
from plone.memoize import forever
from plone.memoize.view import memoize
from plone.memoize.view import memoize_contextless
from plone.tiles import Tile
from ploneintranet import api as pi_api
from ploneintranet.core import _
from ploneintranet.layout.interfaces import IDiazoDashboardTemplate
from ploneintranet.layout.interfaces import IModalPanel
from ploneintranet.search.interfaces import ISiteSearch
from ploneintranet.todo.utils import update_task_status
from ploneintranet.workspace.config import TEMPLATES_FOLDER
from ploneintranet.workspace.utils import parent_workspace
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from urllib.parse import parse_qs
from urllib.parse import urlencode
from urllib.parse import urlparse
from zope.component import getUtility
from zope.interface import implementer
from zope.publisher.browser import BrowserView

import datetime

logger = getLogger(__name__)


@implementer(IBlocksTransformEnabled, IDiazoDashboardTemplate)
class Dashboard(BrowserView):
    """A view to serve as a dashboard for homepage and/or users"""

    _good_dashboards = ["activity", "custom", "task"]
    _dashboards_labels = {
        "activity": _("Activity centric view"),
        "task": _("Task centric view"),
        "custom": _("My view (customizable)"),
    }
    section_class = "section-dashboard"

    @property
    @memoize
    def _tile_class_mapping(self):
        return api.portal.get_registry_record(
            "ploneintranet.layout.dashboard_tile_class_mapping", default={}
        )

    @property
    @memoize
    def _tile_id_mapping(self):
        return api.portal.get_registry_record(
            "ploneintranet.layout.dashboard_tile_id_mapping", default={}
        )

    @property
    @memoize
    def dashboard_options(self):
        """Return the options for the dashboard selector"""
        selected = self.default_dashboard()
        return [
            {
                "value": dashboard,
                "selected": "selected" if dashboard == selected else None,
                "label": self._dashboards_labels.get(dashboard, dashboard),
            }
            for dashboard in self._good_dashboards
        ]

    @property
    @memoize
    def splashpage_uid(self):
        """Get the splashpage uid"""
        return api.portal.get_registry_record(
            "ploneintranet.layout.splashpage_uid", default=""
        )

    @property
    @memoize
    def show_splashpage(self):
        """Check if splashpage should be enabled"""
        if not api.portal.get_registry_record(
            "ploneintranet.layout.splashpage_enabled", default=False
        ):
            return False
        user = self.user
        if not user:
            return False
        if getattr(user, "splashpage_read", None) == self.splashpage_uid:
            return False
        return True

    @property
    @memoize
    def splashpage_content(self):
        """Check if splashpage should be enabled"""
        return api.portal.get_registry_record(
            "ploneintranet.layout.splashpage_content", default=""
        )

    def show_message(self, msg, type="success"):
        """Wrap show message to save some keystrokes"""
        return api.portal.show_message(msg, request=self.request, type=type)

    @property
    @memoize_contextless
    def user(self):
        """Return the current userprofile"""
        return pi_api.userprofile.get_current()

    @memoize_contextless
    def has_custom_dashboard(self):
        """Check if the functionality is allowed for the user"""
        return bool(self.user)

    @forever.memoize
    def tile_options(self, tile):
        """Return the name of the tile from the tile path"""
        qs = tile.partition("?")[-1]
        return parse_qs(qs)

    @forever.memoize
    def get_tile_title(self, tile):
        """Return the title of the tile"""
        key = tile.partition("?")[0]
        view_name = key.split("/")[-1].strip("@")
        view = None
        try:
            view = api.content.get_view(
                name=view_name, context=self.context, request=self.request
            )
        except api.exc.InvalidParameterError:
            logger.warning("Could not get view %", view_name)
        if view:
            title = getattr(view, "title", None)
            if title:
                return title
        # Try for a half decent human readable title, e.g.
        # ./@@events.tile -> Events Tile
        return key.strip("./@").replace(".", " ").replace("_", " ").title()

    @memoize
    def get_tile_display(self, tile):
        """Return the way the tile is displayed"""
        parsed = urlparse(tile)
        if parsed.query:
            for span in parse_qs(parsed.query).get("portletspan", []):
                return span
        if "activitystream.dashboard.tile" in tile:
            default = "span-2"
        else:
            default = "span-1"
        if self.default_dashboard() != "custom":
            return default
        return self.custom_tiles().get(tile, {}).get("display", default)

    @memoize
    def get_tile_classes(self, tile):
        """Try to get the tile classes for the given portlet"""
        classes = []
        key = tile.partition("?")[0].strip("./@")
        if key in self._tile_class_mapping:
            classes.append(self._tile_class_mapping[key])
        else:
            logger.warning("Cannot get class mapping for %r", tile)

        tile_options = self.tile_options(tile)
        if "portletstate" in tile_options:
            # request portletstate takes precedence over registry config
            classes = [cls for cls in classes if not cls.startswith("state")]
            classes.append(tile_options["portletstate"])
        return " ".join(classes)

    @memoize
    def get_tile_id(self, tile):
        """"""
        key = tile.partition("?")[0].strip("./@")
        if key in self._tile_id_mapping:
            value = self._tile_id_mapping[key]
        else:
            value = None
            logger.warning("Cannot get id mapping for %r", tile)
        value = value or DateTime().strftime("portlet-%f")
        tile_options = self.tile_options(tile)
        return "".join([value] + tile_options.get("id_suffix", []))

    @memoize
    def custom_tile_url(self, tile):
        """We need this to set the class parameter span-N"""
        url, qs = tile.partition("?")[::2]
        if "://" not in url:
            url = f"{self.context.absolute_url()}/{url}"
        if not qs:
            return url
        options = self.tile_options(tile)
        if self.default_dashboard() == "custom":
            options.update(portletspan=self.get_tile_display(tile))
        return f"{url}?{urlencode(options, doseq=True)}"

    @memoize_contextless
    def available_custom_tiles(self):
        """This is a list of tiles that can be set in the user profile"""
        return api.portal.get_registry_record(
            "ploneintranet.layout.dashboard_custom_tiles",
        )

    @memoize
    def custom_tiles(self):
        """This is an ordered dict of the tiles from the user profile
        plus the one available
        """
        available = self.available_custom_tiles()
        user_defined = getattr(self.user, "custom_tiles", OrderedDict())
        # The registry record constrains the available tiles. This
        # cleans out possible forward-incompatible older custom
        # definitions.
        for tile in list(user_defined.keys()):  # avoid RuntimeError
            if tile not in available or tile == "":
                user_defined.pop(tile)
        user_defined_keys = [key.partition("?")[0] for key in list(user_defined.keys())]
        for tile in available:
            tile_key = tile.partition("?")[0]
            if tile_key not in user_defined_keys and tile != "":
                user_defined.update({tile: {"display": "span-1"}})
        return user_defined

    def activity_tiles(self):
        """This is a list of tiles taken"""
        return api.portal.get_registry_record(
            "ploneintranet.layout.dashboard_activity_tiles",
        )

    def task_tiles(self):
        """This is a list of tiles taken"""
        return api.portal.get_registry_record(
            "ploneintranet.layout.dashboard_task_tiles",
        )

    def default_dashboard(self):
        """Returns the dashboard name which is set as default in the registry"""
        requested_dashboard = self.request.get("dashboard", "")
        user_dashboard = getattr(self.user, "dashboard_default", "")

        # try to get the dashboard type to display:
        #  1. request has the priority
        #  2. then comes the user profile
        #  3. then the site default stored in the registry
        #  4. fall back on 'activity'
        dashboard = (
            requested_dashboard
            or user_dashboard
            or api.portal.get_registry_record(
                "ploneintranet.layout.dashboard_default", default="activity"
            )
        )
        # before returning the chosen dashboard check if
        # we have a requested value that should be persisted
        # on the user profile
        if (
            self.user
            and requested_dashboard in self._good_dashboards
            and requested_dashboard != user_dashboard
        ):
            self.user.dashboard_default = requested_dashboard

        return dashboard

    def dashboard_tiles(self):
        """Return the list of tiles for the selected dashboard"""
        tiles = []
        dashboard = self.default_dashboard()
        if dashboard == "custom":
            tiles = self.custom_tiles()
        if dashboard == "task":
            tiles = self.task_tiles()
        if dashboard == "activity":
            tiles = self.activity_tiles()
        return [tile for tile in tiles if tile.strip()]

    def maybe_mark_splashpage_as_read(self):
        """Check if we can mark this splash page as read
        If so add an attribute to the user
        """
        splashpage_uid = self.request.form.get("splashpage_uid")
        if not self.user or splashpage_uid != self.splashpage_uid:
            # This is a debug feature to display again the splashpage
            if splashpage_uid == "force":
                self.user.splashpage_read = ""
            return
        self.user.splashpage_read = splashpage_uid

    def __call__(self):
        """Do something before rendering the template"""
        self.maybe_mark_splashpage_as_read()
        return super().__call__()


class NewsTile(Tile):
    index = ViewPageTemplateFile("templates/news-tile.pt")
    title = _("label_title_app_news", default="News")

    def format_date(self, date, length="long"):
        """formats a date localized in long form using zope.i18n
        date must be a datetime or DateTime
        length can be short, medium, long or full, see
        http://bluebream.zope.org/doc/1.0/manual/i18nl10n.html
        We could use this to implement Cornelis' datetime requirements.
        Where would be a good place for such a general method?"""
        locale = self.request.locale
        formatter = locale.dates.getFormatter("date", length=length)
        if isinstance(date, DateTime):
            date = date.asdatetime()
        return formatter.format(date)

    @property
    @memoize_contextless
    def cropText(self):
        """Reuse the homonynous function from the plone view"""
        plone_view = api.content.get_view("plone", self.context, self.request)
        return plone_view.cropText

    def get_cropped_description(self, brain):
        """We want the description to be cropped"""
        length = 120 - len(brain.Title or "")
        return self.cropText(brain.Description, length)

    def get_img_style(self, item, scale="mini"):
        """Return the style for the image tag"""
        if not item.get("has_thumbs"):
            return None
        url = item["url"]
        return "background-image: url({url}/@@images/image/{scale})".format(
            scale=scale, url=url
        )

    @property
    @memoize
    def batch_size(self):
        """Check if we should batch the news_item"""
        if "portlet-news-state-expanded" in self.request.form:
            if self.request.form.get("portlet-news-state-expanded") == "on":
                return 10
            return 4
        if "portletstate" in self.request.form:
            if self.request.form.get("portletstate") == "state-expanded":
                return 10
            return 4
        if "state-expanded" in api.portal.get_registry_record(
            "ploneintranet.layout.dashboard_tile_class_mapping", default={}
        ).get("news.tile", ""):
            return 10
        return 4

    @property
    @memoize_contextless
    def personal_settings(self):
        return api.content.get_view(
            "app-preferences", api.portal.get().apps.preferences, self.request
        )

    @memoize
    def news_items(self):
        pc = api.portal.get_tool("portal_catalog")

        # Check the language that we will use to serve the news
        profile = pi_api.userprofile.get_current()
        language = api.portal.get_current_language()
        if profile:
            language = (
                profile.preferred_content_language or profile.language or language
            )

        query = dict(
            portal_type="News Item",
            sort_on="effective",
            sort_order="reverse",
            languages=language,
        )
        if (
            api.portal.get_registry_record(
                "ploneintranet.layout.filter_news_by_published_state"
            )
            is True
        ):
            # See also upstream issue
            # https://github.com/plone/Products.CMFPlone/pull/4098 but that fix
            # has date granularity, below has time granularity
            now = datetime.datetime.now(datetime.UTC)
            query.update(
                review_state="published",
                effective={"query": now, "range": "max"},
                expires={"query": now, "range": "min"},
            )

        settings = self.personal_settings
        if settings.has_selected_news_section_uids:
            brains = api.content.find(UID=settings.selected_news_section_uids)
            query["path"] = [brain.getPath() for brain in brains]
        results = pc(query)

        batch_size = self.batch_size
        if batch_size:
            results = results[:batch_size]

        items = [
            {
                "title": item.Title,
                "description": self.get_cropped_description(item),
                "url": item.getURL(),
                "uid": item.UID,
                "has_thumbs": item.has_thumbs,
                "item": item,
            }
            for item in results
        ]
        return items

    def can_expand(self):
        return len(self.news_items()) > self.min_num()

    @memoize
    def min_num(self):
        return api.portal.get_registry_record(
            "ploneintranet.layout.min_news_items", default=3
        )

    @memoize
    def max_num(self):
        return api.portal.get_registry_record(
            "ploneintranet.layout.max_news_items", default=10
        )


class TasksTile(Tile):
    index = ViewPageTemplateFile("templates/tasks-tile.pt")
    title = _("label_title_app_todo", default="Todo")

    def render(self):
        return self.index()

    def __call__(self):
        """Display a list of Todo items in the Open workflow state, grouped by
        Workspace and ordered by Due date.
        {'workspace1': {'title': 'WS1', 'url': '...', 'tasks':[<brain>, ...]}}
        """
        pc = api.portal.get_tool("portal_catalog")
        me = api.user.get_current().getId()
        form = self.request.form
        portal = api.portal.get()
        self.task_app = f"{portal.absolute_url()}/apps/todo"

        if self.request.method == "POST" and form:
            return update_task_status(self, return_status_message=True)

        tasks = pc(portal_type="todo", review_state="open", assignee=me, sort_on="due")
        self.grouped_tasks = {}
        for task in tasks:
            obj = task.getObject()
            workspace = parent_workspace(obj)
            # No workspace == personal task
            if workspace is None:
                personal_id = "_personal_tasks"
                if personal_id not in self.grouped_tasks:
                    self.grouped_tasks[personal_id] = {
                        "title": _("Personal tasks"),
                        "url": self.task_app,
                        "tasks": [],
                    }
                self.grouped_tasks[personal_id]["tasks"].append(task)
            else:
                if workspace.id not in self.grouped_tasks:
                    self.grouped_tasks[workspace.id] = {
                        "title": workspace.title,
                        "url": workspace.absolute_url(),
                        "tasks": [],
                    }
                self.grouped_tasks[workspace.id]["tasks"].append(task)
        return self.render()


@implementer(IBlocksTransformEnabled)
class ActivityStreamTile(Tile):
    index = ViewPageTemplateFile("templates/activitystream-tile.pt")
    title = _("Activity stream")

    @property
    @memoize_contextless
    def can_see_newpostbox_tile(self):
        return api.user.has_permission("Plone Social: Add Microblog Status Update")


@implementer(IModalPanel)
class EditDashboard(Dashboard):
    """Edit the dashboard custom tiles"""

    def reset_custom_tiles(self):
        """Reset the custom_tile storage"""
        self.user.custom_tiles = OrderedDict()

    def is_resizable(self, tile):
        """The noresize=1 query string parameter disable the size selection"""
        if "noresize" in self.tile_options(tile):
            return False
        return True

    def allow_fullpage(self, tile):
        """The noresize=1 query string parameter disable the size selection"""
        if "nofullpage" in self.tile_options(tile):
            return False
        return True

    def maybe_update_tiles(self):
        """Update the custom tiles if needed"""
        if not self.has_custom_dashboard():
            self.show_message(_("Function not allowed"), type="error")
            return

        if self.request.form.get("restore", False):
            self.reset_custom_tiles()
            return self.show_message(_("Default dashboard restored"))

        requested_tiles = self.request.form.get("tiles_order") or []
        available = self.available_custom_tiles()
        tiles = OrderedDict()
        for tile in requested_tiles:
            if tile in available:
                tiles[tile] = {
                    "display": self.request.get("display-%s" % tile, "span-1")
                }

        if tiles != self.custom_tiles():
            self.user.custom_tiles = tiles
            return self.show_message(_("Dashboard order changed"))

        return self.show_message(_("Dashboard order unchanged"), type="warning")

    def __call__(self):
        """Update tiles if needed"""
        if self.request.method == "POST":
            self.maybe_update_tiles()
            return self.request.response.redirect(
                self.context.absolute_url() + "/dashboard.html"
            )
        return super().__call__()


class MyDocumentsTile(Tile):
    title = _("Documents")

    @property
    @memoize_contextless
    def max_library_items(self):
        """The maximum number of documents can be controlled
        by a registry record
        """
        return api.portal.get_registry_record(
            "ploneintranet.layout.max_library_items", default=10
        )

    @property
    @memoize_contextless
    def num_library_items(self):
        """The number of documents can be controlled by a registry record"""
        return api.portal.get_registry_record(
            "ploneintranet.layout.num_library_items", default=20
        )

    def get_filters(self):
        """Return the filters that will be used to obtain the documents"""
        filters = {
            "object_provides": [
                IDocument.__identifier__,
                IFile.__identifier__,
                IImage.__identifier__,
            ],
            "-is_archived": True,
            "-trashed": True,
            "review_state": "published",
        }

        path = self.request.form.get("path", "")
        if path and not path.startswith("/"):
            # make the path absolute
            parts = list(api.portal.get().getPhysicalPath())
            parts.append(path)
            filters["path"] = "/".join(parts)
        else:
            # exclude templates
            parts = list(api.portal.get().getPhysicalPath())
            parts.append(TEMPLATES_FOLDER)
            filters["-path_parents"] = "/".join(parts)

        return filters

    def filter_result(self, result):
        """Check if a result is good"""
        try:
            obj = result.getObject()
        except KeyError:
            logger.exception("Cannot get object for result %r", result)
            return False
        now = DateTime()
        try:
            return datify(getattr(obj, "effective_date")) <= now
        except (AttributeError, ValueError, TypeError):
            return True
        except Exception:
            logger.exception("Cannot filter: %r", obj)
        return True

    def my_documents(self):
        """
        Return the most recently modified documents which I have the
        permission to view, optionally filtered by path
        """
        search_util = getUtility(ISiteSearch)
        filters = self.get_filters()
        my_documents = []
        idx = 0
        limit = self.max_library_items
        while len(my_documents) < limit:
            start = idx * limit
            results = tuple(
                search_util.query(
                    filters=filters, sort="-modified", start=start, step=limit
                )
            )
            if not results:
                # There is no point anymore in querying Solr,
                # exit andreturn what we have
                break
            my_documents.extend(list(filter(self.filter_result, results)))
            idx += 1
        return my_documents[:limit]
