#!/usr/bin/python
# -*- coding: utf-8 -*-

# Hive Appier Framework
# Copyright (c) 2008-2024 Hive Solutions Lda.
#
# This file is part of Hive Appier Framework.
#
# Hive Appier Framework is free software: you can redistribute it and/or modify
# it under the terms of the Apache License as published by the Apache
# Foundation, either version 2.0 of the License, or (at your option) any
# later version.
#
# Hive Appier Framework is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Apache License for more details.
#
# You should have received a copy of the Apache License along with
# Hive Appier Framework. If not, see <http://www.apache.org/licenses/>.

__author__ = "João Magalhães <joamag@hive.pt>"
""" The author(s) of the module """

__copyright__ = "Copyright (c) 2008-2024 Hive Solutions Lda."
""" The copyright for the module """

__license__ = "Apache License, Version 2.0"
""" The license for the module """

import time

import appier

from appier_extras import utils


class Prismic(object):
    """
    Partial (mixin) that adds the Prismic CMS proxy functionality
    to a controller or model that inherits from it.

    Most of the methods are meant to be used directly from the
    template or controller.
    """

    _prismic_cache = None
    """ The cache manager that is going to be used store
    prismic values, this should have a huge performance
    impact while accessing the values """

    @classmethod
    def prismic_clear(cls):
        cache = cls._get_prismic_cache()
        cache.clear()

    @classmethod
    def _prismic_deref(cls, entry):
        # verifies if the current value is not a dictionary
        # and that the sys type is not defined in the entry
        # value, if that's the case this is the termination
        # condition and then returns the current entry as the value
        if not isinstance(entry, dict):
            return entry
        if not "type" in entry:
            return entry
        if not "value" in entry:
            return entry

        # retrieves both the type and the (possible) multiple
        # values for the current entry, then retrieves the first
        # value as the "master" value for it
        type = entry["type"]
        values = entry["value"]

        if type == "Text":
            return values

        if type == "Boolean":
            return values

        if type == "Number":
            return values

        if type == "Select":
            return values

        if type == "Image":
            return values["main"]["url"]

        if type == "Link.web":
            return values["url"]

        if type == "StructuredText":
            return "\n".join([value["text"] for value in values])

        if type == "Group":
            group = []
            for value in values:
                value_d = dict(
                    [
                        (key, cls._prismic_deref(value))
                        for key, value in appier.legacy.iteritems(value)
                    ]
                )
                group.append(value_d)
            return group

        return entry

    @classmethod
    def _get_prismic_cache(cls, serialize=True, ref=None):
        ref = ref or Prismic
        if ref._prismic_cache:
            return ref._prismic_cache
        cache_engine = appier.conf("CACHE", "memory")
        cache_engine = appier.conf("CMS_CACHE_ENGINE", cache_engine)
        cache_engine = appier.conf("PRISMIC_CACHE_ENGINE", cache_engine)
        cache_engine = cache_engine.capitalize() + "Cache"
        cache_engine = getattr(appier, cache_engine)
        ref._prismic_cache = cache_engine.new(hash=True)
        if serialize:
            ref._prismic_cache = appier.SerializedCache(cls._prismic_cache)
        return ref._prismic_cache

    @classmethod
    def _filter_params(cls, kwargs):
        params = dict()
        for name in appier.legacy.keys(kwargs):
            if name in ("lang",):
                continue
            params[name] = kwargs.pop(name)
        return params

    def prismic_objects(self, *args, **kwargs):
        kwargs["multiple"] = True
        return self.prismic_value(*args, **kwargs)

    def prismic_value(
        self,
        key,
        default=None,
        cast=None,
        locale=None,
        timeout=86400,
        verify=False,
        multiple=False,
        *args,
        **kwargs
    ):
        # retrieves the reference to the parent class value to be used
        # to access class level methods
        cls = self.__class__

        # tries to retrieve the provided locale value and in case it
        # fails uses the locale associated with the current request
        # as a fallback (optimistic approach)
        locale = locale or self.request.locale

        # in case the cast value is set, tries to resolve it into the appropriate
        # cast operation using the default cast methods in the configuration module
        if cast:
            cast = appier.config._cast_r(cast)

        # in case e the locale value is provided it must be normalized
        # into the format expected by prismic API
        if locale:
            language, territory = locale.split("_", 1)
            locale = language + "-" + territory.upper()
            kwargs["lang"] = locale

        # prefixes the cache's key with locale when retrieving a value for
        # a specific locale, so that proper context is used for caching
        cache_key = locale + ":" + key if locale else key

        # in case there are named arguments provided, assumes that they must
        # be key value pairs for the filtering of the scope and appends the
        # extra filter string to the cache key
        if kwargs:
            cache_key += ":" + ",".join(
                "%s=%s" % (key, str(value))
                for key, value in appier.legacy.iteritems(kwargs)
            )

        # tries to retrieve the reference to the prismic cache engine
        # (singleton instance) and verifies if the key exists in it, returning
        # immediately the value if that's the case
        prismic_cache = cls._get_prismic_cache()
        if cache_key in prismic_cache:
            value = prismic_cache[cache_key]
            if cast and not value == None:
                value = cast(value)
            return value

        # determines if the provided key references a specific field/value on an
        # object or if it instead tries to retrieve the complete object specification
        # and then selects the appropriate retrieval method
        method = self._prismic_value if "." in key else self._prismic_object
        if multiple:
            method = self._prismic_objects

        # retrieve the value remotely and sets the value in the cache engine,
        # to avoid further retrievals later on
        value = method(key, default=default, verify=verify, *args, **kwargs)
        prismic_cache.set_item(cache_key, value, expires=time.time() + timeout)

        # runs the casting operation if required and then returns the final
        # value to the caller method
        if cast and not value == None:
            value = cast(value)
        return value

    def prismic_markdown(
        self,
        key,
        limit=10,
        default=None,
        verify=False,
        encoding="utf-8",
        options=dict(anchors=False, blank=False),
        *args,
        **kwargs
    ):
        value = self.prismic_value(
            key, limit=limit, default=default, verify=verify, *args, **kwargs
        )
        if not value:
            return value
        html_b = utils.MarkdownHTML.process_str(value, options=options)
        html_s = html_b.decode(encoding)
        return html_s

    @property
    def prismic_api(self):
        import prismic

        return prismic.API()

    def _prismic_object(self, *args, **kwargs):
        default = kwargs.get("default", None)
        kwargs["limit"] = 1
        objects = self._prismic_objects(*args, **kwargs)
        return objects[0] if objects else default

    def _prismic_objects(
        self, key, limit=10, default=[], verify=False, *args, **kwargs
    ):
        # retrieves the reference to the parent class value to be used
        # to access class level methods
        cls = self.__class__

        # runs a series of assertions for the provided key, raising exceptions
        # in case one of the pre-conditions is not met
        appier.verify(
            not "." in key,
            message="Malformed key '%s', must include both document type and key" % key,
            code=400,
        )

        # filters the keyword arguments based arguments retrieving only the valid
        # parameters to be used in the filters extension process
        params = cls._filter_params(kwargs)

        # sets the document type value as the provided key as we're trying
        # to retrieve an object instead of a field
        document_type = key

        # retrieves the complete set of items that meet the document type
        # criteria and selects the first one, default to an empty dictionary
        # in case no items exist for such content type
        try:
            query = ['[[at(document.type,"%s")]]' % document_type]
            query.extend(
                [
                    '[[at(my.%s.%s,"%s")]]' % (document_type, key, value)
                    for key, value in appier.legacy.iteritems(params)
                ]
            )
            entries = (
                self.prismic_api.search_documents(
                    q=query, page_size=limit, *args, **kwargs
                )
                or []
            )
        except Exception as exception:
            self.logger.warning("Problem while accessing prismic: %s" % exception)
            return default

        # creates the list that is going to hold the multiple entry
        # maps/objects that are going to store the dereferenced
        # prismic entries (after the filtering)
        entries_m = []

        # iterates over the complete set of entries, to be able to
        # creates the map/object with the dereferenced values
        for entry in entries:
            # creates the entry object/map that is going to be
            # populated with the complete set of dereferenced fields
            entry_m = dict()

            # retrieves the data part of the entry and the
            # field itself according to the document type
            data = entry.get("data", {})
            field = data.get(document_type, {})

            # iterates over the complete set of fields identifiers
            # and values and dereferences the values setting them
            # then in the current entry map/object
            for field_id, field_value in appier.legacy.iteritems(field):
                field_value = cls._prismic_deref(field_value)
                entry_m[field_id] = field_value

            # adds the entry map/object to the current list of entries,
            # this list should represent a simplified structure
            entries_m.append(entry_m)

        # returns the final processed list of entry objects with
        # the complete set of dereferenced values
        return entries_m

    def _prismic_value(self, key, limit=1, default=None, verify=False, *args, **kwargs):
        # retrieves the reference to the parent class value to be used
        # to access class level methods
        cls = self.__class__

        # runs a series of assertions for the provided key, raising exceptions
        # in case one of the pre-conditions is not met
        appier.verify(
            "." in key,
            message="Malformed key '%s', must include both document type and key" % key,
            code=400,
        )

        # filters the keyword arguments based arguments retrieving only the valid
        # parameters to be used in the filters extension process
        params = cls._filter_params(kwargs)

        # splits the provided key around the dot value (namespace oriented)
        # to obtain the document type and the field id
        document_type, field_id = key.split(".", 1)

        # retrieves the complete set of items that meet the document type
        # criteria and selects the first one, default to an empty dictionary
        # in case no items exist for such content type
        try:
            query = ['[[at(document.type,"%s")]]' % document_type]
            query.extend(
                [
                    '[[at(my.%s.%s,"%s")]]' % (document_type, key, value)
                    for key, value in appier.legacy.iteritems(params)
                ]
            )
            entries = (
                self.prismic_api.search_documents(
                    q=query, page_size=limit, *args, **kwargs
                )
                or []
            )
        except Exception as exception:
            self.logger.warning("Problem while accessing prismic: %s" % exception)
            return default

        # retrieves the complete set of items and in case there's at least
        # one returns the first one of it otherwise returns an empty dictionary
        entry = entries[0] if entries else dict()

        # retrieves the complete set of field for the entry and tries to retrieve
        # the requested field (by its identifier)
        data = entry.get("data", {})
        field = data.get(document_type, {})

        # verifies if the requested field exists raising an exception otherwise
        # this should ensure that the value exists in prismic
        if verify:
            appier.verify(
                field_id in field,
                message="'%s' not found" % field_id,
                exception=appier.NotFoundError,
            )

        # retrieves the value of the field requested with the provided identifier
        # defaulting to the provided default value in case it does not exists
        field_value = field.get(field_id, default)

        # runs the dereferencing process for the field value that is going to
        # be retrieved so the proper (expected) value may be returned to caller
        field_value = cls._prismic_deref(field_value)

        # returns the final dereferenced value to the caller method
        # this can be both a plain value or a sequence
        return field_value
