from .. import base
from .interfaces import IConnection
from .interfaces import IConnectionConfig
from .interfaces import IMaintenance
from .interfaces import IQuery
from .interfaces import ISiteSearch
from AccessControl.SecurityManagement import getSecurityManager
from Acquisition import aq_base
from plone import api
from scorched.exc import SolrError
from scorched.search import LuceneQuery
from zope.component import getUtility
from zope.interface import implementer

import collections
import logging
import re
import scorched.search
import six
import six.moves.urllib.parse

logger = logging.getLogger(__name__)


def prepare_data(data):
    """Prepare data (from Plone) for use with SOLR.

    Mutates the supplied mapping in-place.

    :param data: The data mapping.
    :type data: collections.MutableMapping
    """
    arau = data.get("allowedRolesAndUsers")
    if arau is not None:
        arau = list(v.replace(":", "$") for v in arau)
        data["allowedRolesAndUsers"] = arau


class SpellcheckOptions(scorched.search.Options):
    """Alternate SpellcheckOptions implementation.

    This implements sub-options for the Solr spellchecker.
    The scorched implementation just allows turning it on.

    This may be pushed back upstream if deemed successfull.
    """

    option_name = "spellcheck"
    opts = {
        "accuracy": float,
        "collate": bool,
        "maxCollations": int,
        "onlyMorePopular": bool,
        "extendedResults": bool,
        "q": str,
        "reload": bool,
        "build": bool,
    }

    def __init__(self, original=None):
        super().__init__()
        fields = collections.defaultdict(dict)
        self.fields = getattr(original, "fields", fields)

    def field_names_in_opts(self, opts, fields):
        if fields:
            opts[self.option_name] = True


class Search(scorched.search.SolrSearch):
    function_query_boost = base.RegistryProperty(
        "function_query_boost", prefix=__package__
    )

    def _init_common_modules(self):
        super()._init_common_modules()
        self.spellchecker = SpellcheckOptions()

    def spellcheck(self, **kw):
        newself = self.clone()
        spellchecker = newself.spellchecker
        query = kw.get("q", "")
        kw["q"] = query
        spellchecker.update(**kw)
        return newself

    def execute(self, constructor=None):
        """Override the execute method of scorched so that
        we can prepend our boosting. Only boost when doing fulltext search.
        """
        options = self.options()
        boost_query = self.function_query_boost
        if boost_query and "SearchableText" in options["q"]:
            boost = f"{{!boost b={boost_query}}}"
            options["q"] = boost + options["q"]
        ret = self.interface.search(**options)
        if constructor:
            ret = self.constructor(ret, constructor)
        return ret

    def __str__(self):
        return "{}: {} && {}".format(
            repr(self), str(self.query_obj), str(self.filter_obj)
        )


@implementer(IConnectionConfig)
class ConnectionConfig:
    _url = None

    def __init__(self, host, port, basepath, core):
        self.host = host
        self.port = port
        self.basepath = basepath.lstrip("/")
        self.core = core

    @classmethod
    def from_url(cls, url):
        pr = six.moves.urllib.parse.urlparse(url)
        host, port = pr.netloc.split(":", 1)
        basepath, core = list(filter(bool, pr.path.split("/")))
        cfg = cls(host, port, "/" + basepath, core)
        return cfg

    @property
    def url(self):
        if self._url is None:
            format_url = "http://{host}:{port}/{basepath}/{core}".format
            self._url = format_url(**vars(self))
        return self._url


class Connection:
    """A descriptor representing a Solr connection object."""

    def __init__(self):
        self._conn = None

    def __get__(self, obj, objtype):
        if self._conn is None:
            self._conn = IConnection(getUtility(IConnectionConfig))
        return self._conn

    def __del__(self):
        del self._conn


@implementer(IMaintenance)
class Maintenance:
    connection = Connection()

    @classmethod
    def _find_objects_to_index(cls, origin):
        """generator to recursively find and yield all zope objects below
        the given start point"""
        traverse = origin.unrestrictedTraverse
        basepath = "/".join(origin.getPhysicalPath())
        cut = len(base) + 1
        paths = [basepath]
        for idx, path in enumerate(paths):
            obj = traverse(path)
            yield (path[cut:], obj)
            if hasattr(aq_base(obj), "objectIds"):
                for id_ in obj.objectIds():
                    paths.insert(idx + 1, path + "/" + id_)

    def warmup_spellchcker(self):
        """Build the Solr spellchecker."""
        search = IQuery(self.connection)
        response = search.query().spellcheck(build=True).execute()
        return response

    def purge(self, close_connection=False):
        conn = self.connection
        response = conn.delete_all()
        conn.commit(waitSearcher=True, expungeDeletes=True)
        conn.optimize(waitSearcher=True)
        if close_connection:  # only in tests
            conn.conn.http_connection.close()
        return response


@implementer(ISiteSearch)
class SiteSearch(base.SiteSearch):
    """A Site search utility using SOLR as the engine.

    This implementation uses the `scorched` bindings to
    generate the lucene query used to deliver site search results.

    This logic combining the query and filter parameters to the query.

    The resulting logical query is similar to the following:

        ((Title == VALUE OR Description == VALUE OR SearchableText == VALUE)
        AND (FILTER1 == VALUE OR FILTER)
        AND (allowedRolesAndUsers=... OR allowedRolesAndUsers=..) ...

    """

    connection = Connection()
    phrase_field_boosts = base.RegistryProperty(
        "phrase_field_boosts", prefix=__package__
    )
    field_limit = base.RegistryProperty("field_limit", prefix=__package__)

    def lucene_and(self, subqueries):
        query = LuceneQuery()
        query._and = True
        query._or = False
        query.subqueries = subqueries
        return query

    def lucene_or(self, subqueries):
        query = LuceneQuery()
        query._and = False
        query._or = True
        query.subqueries = subqueries
        return query

    def _make_boosted_field_query(self, field, phrase):
        phrase = phrase.strip()
        boost = self.phrase_field_boosts.get(field)
        query = self.lucene_or(
            [self.Q(**{field: word}) for word in phrase.split(" OR ")]
        )
        if boost is not None:
            query **= boost
        return query

    def create_lucene_query(self, phrase):
        """Create a lucene query, boosted and respecting the logical operators

        :param phrase: The phrase to query SOLR with.
        :type phrase: str or iterable
        :returns: A lucene query.
        :rtype query: scorched.search.LuceneQuery
        """
        if not phrase:
            return self.Q()
        if isinstance(phrase, str) and " AND " in phrase:
            phrase = phrase.split(" AND ")
        if isinstance(phrase, str):
            return self.lucene_or(
                [
                    self._make_boosted_field_query(phrase_field, phrase)
                    for phrase_field in self.phrase_fields
                ]
            )
        return self.lucene_and(list(map(self.create_lucene_query, phrase)))

    def _create_query_object(self, phrase):
        """Create the query object given the search `phrase`.

        :param phrase: The phrase to query SOLR with.
        :type phrase: str or iterable
        :returns: A query object.
        :rtype query: ploneintranet.search.solr.search.Search
        """
        lucene_query = self.create_lucene_query(phrase)
        return self._wrap_query_object(self.Q(lucene_query))

    def _wrap_query_object(self, query):
        return IQuery(self.connection).query(query)

    def _apply_filters(self, query, filters):
        if isinstance(filters, LuceneQuery):
            return query.filter(filters)
        Q = self.Q
        if "path" not in filters:
            filters.update(path="/".join(api.portal.get().getPhysicalPath()))
        for key, value in filters.items():
            if key == "path":
                key = "path_parents"
            if "|" in key:
                # create an OR subquery for these | separated keys
                subquery = Q().__or__(Q())
                if isinstance(value, str):
                    value = [value]
                for subkey in key.split("|"):
                    for subvalue in value:
                        subquery.add([Q(**{subkey: subvalue})], {})
                query = query.filter(subquery)
            else:
                if isinstance(value, list):
                    # create an OR subquery for this filter
                    subquery = Q()
                    for item in value:
                        subquery |= Q(**{key: item})
                    query = query.filter(subquery)
                else:
                    query = query.filter(Q(**{key: value}))
        return query

    def _apply_facets(self, query):
        return query.facet_by(fields=self.facet_fields, limit=-1)

    def _apply_boost_query(self, query, boost_query):
        query = query.alt_parser("edismax", bq=boost_query)
        return query

    def _apply_date_range(self, query, start_date, end_date):
        filter_query = query.filter
        if start_date and end_date:
            query = filter_query(created__range=(start_date, end_date))
        elif end_date is not None:
            query = filter_query(created__lt=end_date)
        else:
            query = filter_query(created__gt=start_date)
        return query

    def _apply_spellchecking(self, query, phrase):
        query = query.highlight("Description")
        return query.spellcheck(q=phrase, collate=True, maxCollations=1)

    def _paginate(self, query, start, step):
        return query.paginate(start=start, rows=step)

    def _apply_debug(self, query):
        return query.debug()

    def _get_arau_query(self):
        Q = self.Q
        # _listAllowedRolesAndUsers method requires
        # the actual user object so we can't use plone.api here
        user = getSecurityManager().getUser()
        catalog = api.portal.get_tool(name="portal_catalog")
        arau = catalog._listAllowedRolesAndUsers(user)
        data = dict(allowedRolesAndUsers=arau)
        prepare_data(data)
        # avoid recursion exception by constructing a flat query
        arau_q = Q().__or__(Q())  # construct toplevel OR
        valid_opts = data["allowedRolesAndUsers"]
        sub_qs = [Q(allowedRolesAndUsers=v) for v in valid_opts]
        if len(sub_qs) > 900:
            logger.info(
                "Many arau clauses: {}. Please configure Solr "
                "maxBooleanClauses high enough to avoid SolrError.".format(len(sub_qs))
            )
        # add subqueries directly on toplevel, refs #695
        arau_q.add(sub_qs, {})
        return arau_q

    def _apply_security(self, query):
        # you can check validity with: str(arau_q)
        return query.filter(self._get_arau_query())

    @classmethod
    def _collect_query_params(cls, iface, bucket):
        """Collect original query parameters for debugging purposes.

        :param iface: The Zope interface used for the query.
        :type iface: zope.interface.Interface
        :param bucket: The container which field names will be inserted.
        :type bucket: collections.MutableMapping
        """
        params = collections.OrderedDict()
        query_spec = iface["query"]
        for name in query_spec.required:
            params[name] = bucket[name]
        for name in query_spec.optional:
            if name in bucket:
                params[name] = bucket[name]
        return params

    def execute(self, query, secure=True, **kw):
        if secure:
            query = self._apply_security(query)
        sort = kw.get("sort")
        if sort:
            # valid sort values:
            #  - 'field': sort results ascending by field
            #  - '-field': sort results descending by field
            if isinstance(sort, str):
                sort = [sort]
            for field in sort:
                query = query.sort_by(field)
        if self.field_limit:
            query = query.field_limit(self.field_limit)

        try:
            response = query.execute()
        except SolrError as exc:
            try:
                message = f"{exc.message} on query: {query.debug().options()}"
            except AttributeError:
                # Try to see if we have a response text from Solr
                message = exc.args[0].text

            raise SolrError(message)
        if kw.get("debug"):
            # you will have to reformat path_parents for solr console queries
            # path_parents:\\/Plone\\/foo -> path_parents:"/Plone/foo"
            logger.info(query.debug().options())
        query_params = self._collect_query_params(ISiteSearch, dict(kw))
        response.query_params = query_params
        return response

    @property
    def Q(self):
        """Forward reference to the :py:class:`LuceneQuery` factory."""
        return self.connection.Q

    def check_negatives(self, phrase, filters):
        """Filter out from the search results all the words
        prefixed by an hyphen
        """
        pattern = re.compile(r"(^| )-([\w-]+)")
        negative = " ".join(match[1] for match in pattern.findall(phrase)).strip()
        if negative:
            filters["-SearchableText"] = negative
            phrase = pattern.sub("", phrase).strip()
        return 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,
    ):
        if isinstance(phrase, str):
            phrase, filters = self.check_negatives(phrase, filters or {})
        return super().query(
            phrase=phrase,
            filters=filters,
            query=query,
            boost_query=boost_query,
            start_date=start_date,
            end_date=end_date,
            start=start,
            step=step,
            sort=sort,
            debug=debug,
            secure=secure,
        )
