from datetime import datetime
from datetime import timedelta
from logging import getLogger
from plone import api
from plone.app.textfield.value import RichTextValue
from plone.i18n.normalizer.interfaces import IIDNormalizer
from plone.memoize import ram
from plone.memoize.view import memoize
from plone.memoize.view import memoize_contextless
from ploneintranet import api as pi_api
from ploneintranet.core import _
from ploneintranet.layout.app import apps_container_id
from ploneintranet.layout.browser.proto import _5min_cache_key
from ploneintranet.layout.interfaces import IDiazoAppTemplate
from ploneintranet.search.interfaces import IExcludeFromSearchResults
from ploneintranet.search.interfaces import ISiteSearch
from Products.Five import BrowserView
from zope.component import getUtility
from zope.interface import implementer
from ZTUtils import make_query

import locale

logger = getLogger(__name__)


@implementer(IDiazoAppTemplate)
class SearchResultsView(BrowserView):
    title = _("label_title_app_search", default="Search")
    # Map portal types to url fragments so that the correct sidebar
    # section is opened
    url_fragments = {
        "Document": "#workspace-documents",
        "Image": "#workspace-documents",
        "File": "#workspace-documents",
        "todo": "#workspace-tickets",
        "Event": "#workspace-events",
    }

    _default_preview_class = "file"
    _batch_size = 10

    def _extract_date(self, field):
        """Convert the string passed by pat-date-picker
        in to a datetime object
        """
        try:
            return datetime.strptime(self.request.form.get(field, ""), "%Y-%m-%d")
        except ValueError:
            return

    @property
    @memoize_contextless
    def proto_view(self):
        """Return the proto view"""
        return api.content.get_view("proto", self.context, self.request)

    @property
    @memoize
    def user(self):
        """The current user (if we have one authenticated), or None"""
        return pi_api.userprofile.get_current()

    def page_number(self):
        """Get current page number from the request"""
        try:
            page = int(self.request.form.get("page", 1))
        except ValueError:
            page = 1
        return page

    def get_start(self):
        """
        Fills the start parameter of the search query,
        i.e. the first element of the batch
        """
        return (self.page_number() - 1) * self._batch_size

    def next_page_number(self, total_results):
        """Get page number for next page of search results"""
        page = self.page_number()
        if page * self._batch_size < total_results:
            return page + 1
        else:
            return None

    def next_page_url(self, total_results):
        """Get url for the next page of results"""
        next_page_number = self.next_page_number(total_results)
        if not next_page_number:
            return
        new_form = self.request.form.copy()
        advanced = []
        for key, value in list(new_form.items()):
            if isinstance(value, list) and any(
                [hasattr(entry, "items") for entry in value]
            ):
                # make_query doesn't create ':records' entries for lists of
                # dicts
                for subdict in value:
                    for subkey, subvalue in subdict.items():
                        new_key = f"{key}.{subkey}:records"
                        advanced.append(make_query({new_key: subvalue}))
                del new_form[key]
        new_query = make_query(new_form, {"page": next_page_number})
        new_query = "&".join(advanced + [new_query])
        return f"{self.request.ACTUAL_URL}?{new_query}"

    def friendly_path(self, url):
        """Show the object path within the navigation root"""
        root_url = api.portal.get_navigation_root(self.context).absolute_url()
        return url.replace(root_url, "")

    @ram.cache(_5min_cache_key)
    def preview_class(self, portal_type, friendly_type_name):
        """Get the matching preview class according to portal type.

        If found on the mapping return the key, otherwise return a default.
        """
        mapping = self.preview_class_mapping
        for key in mapping:
            if portal_type in mapping[key]:
                if key == "file":
                    return self.proto_view.translate_friendly_type(friendly_type_name)
                return key
        return self._default_preview_class

    @property
    @ram.cache(_5min_cache_key)
    def workspace_type_mapping(self):
        """Return the mapping between workspace types and css types
        as defined in the registry record
        ploneintranet.workspace.workspace_types_css_mapping
        """
        workspace_types_css_mapping = api.portal.get_registry_record(
            "ploneintranet.workspace.workspace_types_css_mapping",
            default="ploneintranet.workspace.case|type-case",
        )
        css_mapping = dict(
            line.partition("|")[::2] for line in workspace_types_css_mapping
        )
        return css_mapping

    @property
    @ram.cache(_5min_cache_key)
    def preview_class_mapping(self):
        mapping = {
            "app": ("ploneintranet.layout.app", "ploneintranet.news.app"),
            "user": ("ploneintranet.userprofile.userprofile",),
            "group": ("ploneintranet.userprofile.workgroup",),
            "rich": ("Document",),
            "event": ("Event",),
            "workspace": ("ploneintranet.workspace.workspacefolder",),
            "file": (
                "ploneintranet.userprofile.userprofilecontainer",
                "todo",
                "Folder",
                "File",
                "Image",
                "News Item",
            ),
            "email": ("ploneintranet.workspace.mail",),
        }
        mapping["workspace"] += tuple(self.workspace_type_mapping.keys())
        return mapping

    def get_text_preview(self, result):
        """Given a search result return a preview"""
        try:
            obj = result.getObject()
        except KeyError:
            logger.exception("Cannot not get object for result %r", result)
            return ""
        text = getattr(obj, "text", None)
        if isinstance(text, RichTextValue):
            return text.raw
        return text or ""

    @memoize
    def get_keywords(self):
        """Return the keywords we are searching"""
        keywords = self.request.form.get("SearchableText")
        if isinstance(keywords, list):
            keywords = [keyword for keyword in keywords if keyword]
        return keywords

    @memoize
    def is_advanced_search(self):
        return self.request.form.get("search-mode") == "advanced"

    def is_searching(self):
        """Check if the user is searching, i.e. we have something to query in
        SearchableText or search-mode is advanced
        """
        return bool(self.get_keywords() or self.is_advanced_search())

    def is_filtering(self):
        """Check if the user is filtering, i.e. "filtering" is trueish"""
        return bool(self.request.get("filtering"))

    @property
    @memoize
    def supported_filters(self):
        """The list of supported filters as defined in the registry"""
        return api.portal.get_registry_record("ploneintranet.search.filter_fields")

    def get_filters(self):
        """Return the filters for this search"""
        form = self.request.form
        exclude_interface = IExcludeFromSearchResults.__identifier__
        filters = {"!object_provides": exclude_interface}
        for key in self.supported_filters:
            value = form.get(key, "")
            if value:
                filters[key] = value
        if "portal_type" not in filters:
            pu = api.portal.get_tool("plone_utils")
            filters["portal_type"] = pu.getUserFriendlyTypes()
        if not form.get("include_archived"):
            filters["!is_archived"] = True
        return filters

    @memoize
    def get_additional_facets(self):
        """Returns additional facets as read from the registry

        Example: [{'id': u'tags', 'label': u'Tags'}, ...]
        """
        record = api.portal.get_registry_record(
            "ploneintranet.search.ui.additional_facets", default={"tags": "Tags"}
        )
        class_record = api.portal.get_registry_record(
            "ploneintranet.search.ui.additional_facet_classes", default={"tags": "tags"}
        )
        supported_filters = self.supported_filters

        facets = [
            {"id": key, "label": record[key], "class": class_record.get(key, key)}
            for key in supported_filters
            if key in record
        ]
        return facets

    @memoize
    def get_sorting(self):
        """Get the requested sorting method

        Supported methods:
         - relevancy (default, returns None)
         - reverse creation date (returns '-created')
        """
        user_default = getattr(self.user, "search_sorting", "relevancy")

        # Get sorting method
        sorting = self.request.get("results-sorting", user_default)
        if sorting != user_default:
            self.user.search_sorting = sorting

        if sorting == "date":
            return "-created"

    def reset_button_pat_depends(self):
        """Return the pat_depends expression for the pat-depends
        that displays the reset button
        """
        inputs = ["friendly_type_name", "include_archived"]
        for facet in self.get_additional_facets():
            inputs.append(facet["id"])
        return "condition: {condition}; transition: slide".format(
            condition=" or ".join(inputs)
        )

    def get_query_type_by_field(self, field):
        query_type_by_field = {
            "Title": "lemma",
            "Description": "lemma",
            "SearchableText": "lemma",
            "modified": "date",
            "created": "date",
        }
        return query_type_by_field.get(field, "lemma")

    def get_date_query_parameters(self, key, value, operator):
        value = datetime.strptime(value, "%Y-%m-%d")
        if operator == "is":
            key = key + "__range"
            value = (value, value + timedelta(1) - timedelta(0, 1))
        elif operator == "is-before":
            key = key + "__lt"
        elif operator == "is-after":
            key = key + "__gt"
        return key, value

    def _get_advanced_query(self, Q):
        query = Q()
        rules = self.request.form.get("search-rule", [])
        for index, rule in enumerate(rules):
            if index == 0 and rule["and-or"]:
                raise ValueError("First rule can not have an operator")

            query_type = self.get_query_type_by_field(rule["data-field"])

            key = rule["data-field"]
            value = rule[query_type + "-value"]
            operator = rule[query_type + "-operator"]

            if not value:
                continue

            if query_type == "date":
                key, value = self.get_date_query_parameters(key, value, operator)

            subquery = Q(**{key: value})

            if operator == "contains-not":
                subquery = ~subquery
            elif operator == "equals":
                logger.warning(
                    "Operator {} is not supported, falling back "
                    "to default".format(operator)
                )

            if index == 0:
                query = subquery
                continue
            elif rule["and-or"] == "and":
                query &= subquery
            elif rule["and-or"] == "or":
                query |= subquery
        return query

    @property
    def search_util(self):
        return getUtility(ISiteSearch)

    @memoize
    def search_response(self):
        """Parse the parameters from the request
        and query the ISiteSearch utility
        """
        if not self.is_searching():
            return []
        search_util = self.search_util
        if self.is_advanced_search():
            query = self._get_advanced_query(search_util.Q)
        else:
            query = None

        response = search_util.query(
            phrase=self.get_keywords(),
            filters=self.get_filters(),
            query=query,
            boost_query=(
                "{!func}if(termfreq(friendly_type_name,'Emergency Service'),1024.0,1.0)"
            ),
            start_date=self._extract_date("start_date"),
            end_date=self._extract_date("end_date"),
            start=self.get_start(),
            step=self._batch_size,
            sort=self.get_sorting(),
        )
        return response

    def sorting_key_item_title(self, item):
        """A sorting key for an item that has a title

        PY3 only
        """
        return locale.strxfrm(item["title"])

    @memoize
    def get_facets(self, facet):
        """Return the tags for faceting search results"""
        response = self.search_response()
        items = [
            {
                "id": getUtility(IIDNormalizer).normalize(t["name"]),
                "title": t["name"],
                "counter": t["count"],
            }
            for t in response.facets.get(facet, [])
        ]
        # BBB: I am commenting the following method
        # because we do not show the number of matching results per tags.
        # The order coming for the commented line will appear as disorder
        # to the user
        # tags.sort(key=lambda t: (-t['counter'], t['title'].lower()))

        # Sorting with locales changed a lot in PY3. Mixed with the missing cmp in PY3
        # it means that we have to do a very special thing here
        items.sort(key=self.sorting_key_item_title)
        return items

    @memoize
    def type_facets(self):
        """Return the types for faceting search results"""
        response = self.search_response()
        types = response.facets.get("friendly_type_name", [])
        if len(types) < 2:
            # no point in showing facets if there is only one value to pick
            return []
        types = [
            {
                "id": self.proto_view.friendly_type2type_class(t["name"]),
                "title": t["name"],
                "counter": t["count"],
            }
            for t in types
        ]
        # BBB: I am commenting the following method
        # because we do not show the number of matching results per facet.
        # The order coming for the commented line will appear as disorder
        # to the user
        # types.sort(key=lambda t: (-t['counter'], t['title'].lower()))

        # Sorting with locales changed a lot in PY3. Mixed with the missing cmp in PY3
        # it means that we have to do a very special thing
        types.sort(key=self.sorting_key_item_title)
        return types

    @memoize
    def display_previews(self):
        """Check if we have to display previews.

        According to the prototype this is controlled
        by the state of the checkbox 'display-previews'
        that appears in the options tooltip.
        The value is stored on the user object.

        """
        user_default = getattr(self.user, "display_previews", "on")

        # When we are not filtering we accept the user_default
        if not self.request.get("options.submitted"):
            return user_default == "on"

        display_previews = self.request.get("display-previews")
        if user_default != display_previews:
            self.user.display_previews = display_previews

        return display_previews == "on"

    def search_by_type(self, type_name, allow_all=False, sort=None):
        """
        Search for specific content types
        """
        form = self.request.form
        filters = {"portal_type": type_name}
        search_util = self.search_util

        if self.is_advanced_search():
            query = self._get_advanced_query(search_util.Q)
            keywords = None
        else:
            query = None
            keywords = form.get("SearchableText")
            if not keywords:
                if not allow_all:
                    return []

        response = search_util.query(
            phrase=keywords,
            filters=filters,
            query=query,
            boost_query=(
                "{!func}if(termfreq(friendly_type_name,'Emergency Service'),1024.0,1.0)"
            ),
            start=self.get_start(),
            step=self._batch_size,
            sort=sort,
        )
        return response

    def search_images(self, allow_all=False, sort=None):
        return self.search_by_type("Image", allow_all=allow_all, sort=sort)

    def search_files(self, allow_all=False, sort=None):
        return self.search_by_type("File", allow_all=allow_all, sort=sort)

    def search_people(self, allow_all=False, sort=None):
        return self.search_by_type(
            ["ploneintranet.userprofile.userprofile", "ploneintranet.contacts.contact"],
            allow_all=allow_all,
            sort=sort,
        )


class SearchUtilityView(SearchResultsView):
    """The search utility remains bound to the site root, whereas the user-
    facing search view has been moved to the Search App.
    """

    def __call__(self):
        """
        In case somebody calls @@search in the site root, redirect to the
        App.
        """
        portal = api.portal.get()
        app = getattr(portal, apps_container_id).search
        self.request.RESPONSE.redirect(app.absolute_url())


class UserSearchResultsView(SearchResultsView):
    """The search view in the context of the user"""

    def is_searching(self):
        """Return all the results
        even if we do not submit any SearchableText,
        otherwise we will an ugly looking blank page
        """
        return True


class ContactSearchResultView(BrowserView):
    """A view to display a contact search result

    The context is a SearchResult object
    """

    @property
    @memoize
    def result_object(self):
        """Return the context of the view"""
        return self.context.getObject()

    def is_emergency_service(self) -> bool:
        """Check if the contact is an emergency service"""
        return self.context.friendly_type_name == "Emergency Service"

    @property
    @memoize
    def card_css_classes(self) -> str:
        """Return the css classes for the contact card"""
        css_classes = ["user-card"]
        if self.is_emergency_service():
            css_classes.append("emergency-service")
        return " ".join(css_classes)

    @property
    @memoize
    def card_icon(self) -> str:
        """Return card icon based on the contact type"""
        if self.is_emergency_service():
            return f"icon-{self.result_object.service_icon}"
        return ""

    @property
    @memoize
    def byline(self) -> str:
        """The byline for the contact result"""
        if self.is_emergency_service():
            return self.context.description or ""
        return self.context.job_title or ""

    @property
    @memoize
    def email(self) -> str:
        """Return the email of the contact"""
        return self.context.contact_email or ""

    @property
    @memoize
    def phone(self) -> str:
        """Return the phone number of the contact"""
        return self.context.contact_telephone or ""

    @property
    @memoize
    def mobile(self) -> str:
        """Return the mobile number of the contact"""
        return self.context.contact_mobile or ""
