"""Abstract basis for search implementations."""

from .interfaces import ISearchResponse
from .interfaces import ISearchResult
from Acquisition import aq_base
from collections.abc import Iterable
from plone import api
from plone.api.validation import at_least_one_of
from plone.memoize import instance
from zope.globalrequest import getRequest

import abc
import logging

logger = logging.getLogger(__name__)


# A 'constant' object that evaluates to False
class FeatureNotImplemented:
    def __bool__(self):
        return False

    __nonzero__ = __bool__


FEATURE_NOT_IMPLEMENTED = FeatureNotImplemented()


class RegistryProperty:
    """A descriptor that gets and sets values from Plone's registry."""

    def __init__(self, field_name, prefix=__package__):
        self._key = f"{prefix}.{field_name}"

    def __get__(self, obj, type=None):
        return api.portal.get_registry_record(self._key)

    def __set__(self, obj, val):
        api.portal.set_registry_record(self._key, val)


class NoOpQueryMethod:
    """A non-data descriptor that indicates a query feature is not applicable.

    This could either be because a given feature is not supported,
    or that the back-end in use already supports the operation in some
    other way.

    Assigning an instance of this class to a method name which usually
    forms part of an abstract API will result in a `pass through` call.
    """

    def __init__(self, reason="Not supported by backend"):
        self._reason = reason

    def __get__(self, obj, obj_type=None):
        reason = self._reason
        if not reason.rstrip().endswith(":"):
            self._reason = reason.format(obj.__module__)
        return self._no_op

    def __repr__(self):
        return f"{self.__class__.__name__}(0._reason)"

    def __str__(self):
        return self._reason

    def _no_op(self, query, *args, **kw):
        return query


class SearchResult:
    """Abstract search result implementation.

    Builds a Search result from a generic search result
    and an ISearchResponse.
    This allows result objects to access extra data from
    the ISearchResponse when necessary.
    """

    highlighted_summary = FEATURE_NOT_IMPLEMENTED

    _app_portal_types = ("ploneintranet.layout.app", "ploneintranet.news.app")

    def __init__(self, context, response):
        self.context = context
        self.response = response
        self.title = context["Title"]
        self.description = context.get("Description", "")
        self.friendly_type_name = context["friendly_type_name"]
        self.portal_type = context["portal_type"]
        self.contact_email = context.get("email")
        self.contact_telephone = context.get("telephone")
        self.contact_mobile = context.get("mobile")
        self.job_title = context.get("job_title")
        self.modified = context["modified"]
        self.UID = context["UID"]
        self.start = context.get("start", None)
        self.end = context.get("end", None)
        self.location = context.get("location", "")
        self.invitees = context.get("invitees", [])
        self.timezone = context.get("timezone", False)
        self.whole_day = context.get("whole_day", False)
        self.ws_type = context.get("ws_type", "workspace")
        # The following try/except are needed because the get method of brains
        # and the one from dicts behave differently
        try:
            self.is_archived = context["is_archived"]
        except KeyError:
            self.is_archived = False
        try:
            self.archival_date = context["archival_date"]
        except KeyError:
            self.archival_date = None

    @property
    @instance.memoize
    def mimetype(self):
        """Lazy load mymetype from a brain or a solr search result"""
        return getattr(self.context, "mimetype", None) or self.context.get(
            "mimetype", None
        )

    @property
    @instance.memoize
    def preview_image_path(self):
        """Lazy load preview urls"""
        if self.portal_type == "Image":
            return f"{self.path}/@@images/image/preview"
        if self.portal_type == "ploneintranet.userprofile.userprofile":
            return f"{self.path}/@@avatar_profile.jpg"
        if self.context.get("has_thumbs"):
            # can occur in workspaces AND library
            if self.portal_type in ("Document", "News Item"):
                return f"{self.path}/@@images/image/preview"
            return f"{self.path}/@@render.svg"
        return ""

    def __repr__(self):
        clsnam = type(self).__name__
        state = vars(self)
        attrs = {
            name: state.get(name) for name in dict(ISearchResult.namesAndDescriptions())
        }
        format_item = "{name}={value!r}".format
        attr_state = ", ".join(
            format_item(name=name, value=value) for (name, value) in attrs.items()
        )
        return f"{clsnam}({attr_state})"

    __str__ = __repr__

    def _path_to_url(self, physical_path):
        """
        Generate an absolute url from a physical path
        """
        if not physical_path:
            return
        request = getRequest()
        if not request:
            return
        return request.physicalPathToURL(physical_path)

    @property
    def path(self):
        """Return the path URI to the object represented."""

    def getPath(self):
        return self.path

    @property
    def getId(self):
        return self.context["getId"]

    @property
    def url(self):
        """Generate the absolute URL for the indexed document.

        :return: The absolute URL to the document in Plone
        :rtype: str
        """
        view_types = api.portal.get_registry_record(
            "plone.types_use_view_action_in_listings"
        )
        url = self._path_to_url(self.path)
        if self.context["portal_type"] in view_types:
            url = f"{url}/view"
        return url

    def getURL(self):
        return self.url

    def Subject(self):
        return self.context.get("Subject", ())

    @property
    def preview_image_url(self):
        """
        Generate the absolute URL for the preview image of the indexed document

        :returns: The absolute URL to the preview image
        :rtype: str
        """
        if self.portal_type in self._app_portal_types:
            request = getRequest()
            if not request:
                return ""
            obj = self.getObject()
            view = api.content.get_view("app-tile", obj, request)
            return view.icon_url
        path = self.preview_image_path
        return path and self._path_to_url(path) or ""

    def __getitem__(self, key):
        value = getattr(aq_base(self), key, False)
        if value and callable(value):
            return value()
        return self.context[key]

    def get(self, key, default=None):
        try:
            return self[key]
        except KeyError:
            return default


class SearchResponse(Iterable):
    """Base search response object"""

    total_results = FEATURE_NOT_IMPLEMENTED
    spell_corrected_search = FEATURE_NOT_IMPLEMENTED
    facets = FEATURE_NOT_IMPLEMENTED

    def __init__(self, context):
        super().__init__()
        self.context = context

    def __iter__(self):
        yield from self.context

    def __bool__(self):
        return bool(self.total_results)

    __nonzero__ = __bool__

    def __len__(self):
        return self.total_results

    @property
    def results(self):
        return self.context


class SiteSearchProtocol(metaclass=abc.ABCMeta):
    """Abstract site search query protocol.

    Implementations of ISiteSearch should register themselves
    with this abc:

        SiteSearchProtocol.register(MySiteSearchImplementation)

    Should an implementation not be able to implement one of the abstract
    methods defined here, it should explicitly declare so.

    Example:

        @implementer(ISiteSearch)
        class MySiteSearch(object):

            _apply_spellchecking = QueryOperationNotApplicable()

    """

    @abc.abstractmethod
    def _create_query_object(self, phrase):
        """Create a scorched query object for the given ``phrase``.

        Apply any boost parameters based on plone.app.registry entries.

        :return: The query object to be executed from the given ``phrase``.
        """

    @abc.abstractmethod
    def _apply_filters(self, query, filters):
        """Build a filtered phrase query.

        Ensure filters are a subset of those configured via plone.app.registry.

        :return: A query object that can be further filtered.
        """

    @abc.abstractmethod
    def _apply_facets(self, query):
        """Apply parameters such that the query response will be faceted.

        Ensure facets are a subset of those configured via plone.app.registry.

        :return: A copy of the modified `query`.
        """

    @abc.abstractmethod
    def _apply_date_range(self, query, start_date=None, end_date=None):
        """Optionally apply a date range filter to the query.

        `start_date` and `end_date` will never both be `None`.

        :return: A copy of the query with date range applied.
        """

    @abc.abstractmethod
    def _paginate(self, query, start, step):
        """Paginate the query object by ``start`` and ``step``.

        :return: A copy of the query object with `start` and `step` pagination
                 parameters applied.
        """

    @abc.abstractmethod
    def _apply_spellchecking(self, query, phrase):
        """Optionally apply parameters such that the given ``phrase`` is spell-checked.

        Return a copy of the modified `query`.
        """

    @abc.abstractmethod
    def _apply_boost_query(self, query, boost_query):
        """Optionally apply boosts to documents matching a query.

        :return: A copy of the query with the boost query applied.
        """

    @abc.abstractmethod
    def _apply_debug(self, phrase, query):
        """Add any available debugging to the query.

        Return a copy of the modified `query`.
        """

    @abc.abstractmethod
    def query(
        self,
        phrase=None,
        filters=None,
        query=None,
        boost_query=None,
        start_date=None,
        end_date=None,
        start=0,
        step=None,
        debug=False,
    ):
        """Implement a site search query protocol.

        Return an object implementing `ISearchResponse`.

        :seealso: ploneintranet.search.interfaces.ISearchResponse
        :seealso: ploneintranet.search.interfaces.ISiteSearch.query
        """

    @abc.abstractmethod
    def execute(self, query, secure=True, **kw):
        """Execute the query.

        Return an object that can be adapted to ISearchResponse.

        Subclasses should provide the means to filter results based on a
        security context, which should be on by default.

        :param query: The query object.
        :type query: scorched.search.Search
        :param secure: Whether or not to apply security to search results.
        :type secure: bool
        """


class SiteSearch:
    """Defines the default SiteSearch query implementation.

    Implementations should sub-class this abstract class
    in order to implement the default site search algorithm.
    """

    facet_fields = RegistryProperty("facet_fields")
    phrase_fields = RegistryProperty("phrase_fields")

    @at_least_one_of("query", "phrase", "filters")
    def query(
        self,
        phrase=None,
        filters=None,
        query=None,
        boost_query=None,
        start_date=None,
        end_date=None,
        start=0,
        step=None,
        sort=None,
        debug=False,
        secure=True,
    ):
        """Return a search response.

        :seealso: ploneintranet.search.interfaces.ISearchResponse
        """
        if query is None:
            query = self._create_query_object(phrase)
        else:
            query = self._wrap_query_object(query)
        if filters is not None:
            query = self._apply_filters(query, filters)
        query = self._apply_facets(query)
        if phrase is not None:
            query = self._apply_spellchecking(query, phrase)
        if boost_query is not None:
            query = self._apply_boost_query(query, boost_query)
        if any((start_date, end_date)):
            query = self._apply_date_range(query, start_date, end_date)
        if step is not None:
            query = self._paginate(query, start, step)
        if debug:
            query = self._apply_debug(query)

        response = self.execute(
            query,
            phrase=phrase,
            start_date=start_date,
            end_date=end_date,
            start=start,
            step=step,
            sort=sort,
            debug=debug,
            secure=secure,
        )
        return ISearchResponse(response)


SiteSearchProtocol.register(SiteSearch)
