from Acquisition import aq_parent
from DateTime import DateTime
from logging import getLogger
from plone import api
from plone.app.event.base import first_weekday
from plone.app.z3cform.widgets.radio import RadioWidget
from plone.app.z3cform.widgets.singlecheckbox import SingleCheckBoxBoolWidget
from plone.i18n.normalizer import idnormalizer
from plone.memoize import ram
from plone.memoize.view import memoize
from plone.memoize.view import memoize_contextless
from plone.protect.authenticator import createToken
from plone.resource.file import FilesystemFile
from ploneintranet import api as pi_api
from ploneintranet.core import _
from ploneintranet.layout.widgets.checkbox import PIMinimalCheckBoxWidget
from ploneintranet.layout.widgets.select import PISelectWidget
from ploneintranet.workspace.utils import parent_workspace
from Products.Five import BrowserView
from time import time
from urllib.parse import urlencode
from z3c.form.browser.checkbox import CheckBoxWidget
from zope.deprecation import deprecate
from zope.i18nmessageid import MessageFactory

import importlib
import json
import packaging

pl_message = MessageFactory("plonelocales")

logger = getLogger(__name__)


def _forever_cache_key(func, self, *args, **kwargs):
    """Cache this function call forever.
    String arguments case will be ignored.
    """
    try:
        portal_url = api.portal.get().absolute_url()
    except api.exc.CannotGetPortalError:
        portal_url = ""
    return (
        portal_url,
        func.__name__,
        [arg.lower() if isinstance(arg, str) else args for arg in args],
        list(kwargs.items()),
    )


def _5min_cache_key(func, self, *args, **kwargs):
    """Cache this function call for 5 minutes.
    String arguments case will be ignored.
    """
    args = args + (time() // 300,)
    return _forever_cache_key(func, self, *args, **kwargs)


# Map to get non-localized month abbreviation from a month number.
MONTH_TO_ABBR_MAP = {
    1: "jan",
    2: "feb",
    3: "mar",
    4: "apr",
    5: "may",
    6: "jun",
    7: "jul",
    8: "aug",
    9: "sep",
    10: "oct",
    11: "nov",
    12: "dec",
}


class ProtoView(BrowserView):
    """A view that contains prototype related utilities"""

    validation_start_after_end_error_message = _(
        "This date must be on or before the end date."
    )
    validation_end_before_start_error_message = _(
        "This date must be on or after the start date."
    )

    # The widgets will get the following classes
    # when rendered with the default macros
    widget_class_mapping = {
        SingleCheckBoxBoolWidget: "group pat-toggle-switch",
        (RadioWidget, CheckBoxWidget, PIMinimalCheckBoxWidget): "group",
        PISelectWidget: "pat-select",
    }

    # The following widgets will be rendered within a fieldset
    # when rendered with the default macros
    widgets_with_fieldset = (SingleCheckBoxBoolWidget, RadioWidget, CheckBoxWidget)

    @ram.cache(_forever_cache_key)
    def translate_friendly_type(self, value):
        """Translate the Plone intranet friendly_type_name into a
        name that fits the prototype conventions
        """
        value = value.lower()
        if "app" in value:
            return "app"
        if "word" in value or "odt document" in value or "rtf" in value:
            return "word"
        if "excel" in value or "ods spreadsheet" in value or "csv" in value:
            return "excel"
        if "pdf" in value:
            return "pdf"
        if "page" in value:
            return "rich"
        if "news" in value:
            return "news"
        if "event" in value:
            return "event"
        if "image" in value:
            return "image"
        if "presentation" in value:
            return "powerpoint"
        if "workspace" in value:
            return "workspace"
        if "project" in value:
            return "workspace"
        if "link" in value:
            return "link"
        if "question" in value:
            return "question"
        if "audio" in value:
            return "audio"
        if "video" in value:
            return "video"
        if "contract" in value:
            return "contract"
        if "odt" in value:
            return "odt"
        if "document" in value:
            return "odt"
        if "openoffice" in value:
            return "odt"
        if "octet" in value:
            return "file"
        if "postscript" in value:
            return "postscript"
        if "plain" in value:
            return "plain-text"
        if "archive" in value:
            return "zip"
        if "business card" in value:
            return "business-card"
        if "userprofilecontainer" in value:
            return "folder"
        if "todo" in value:
            return "task"
        if "folder" in value:
            return "folder"
        if "library.section" in value:
            return "library-section"
        if "library.folder" in value:
            return "library-subsection"
        # we don't have a way to distinguish Document in library yet
        # that would become 'library-item
        if "superspace" in value:
            return "superspace"
        if "mail" in value:
            return "email"
        if (
            "profile" in value
            or "person" in value
            or "contact" in value
            or "people" in value
        ):
            return "people"
        if "file" in value:
            return "file"
        if "group" in value:
            return "group"
        if "emergency" in value:
            return "emergency-service"
        # This is our fallback
        logger.warning("Unrecognized friendly type: %s", value)
        return "file"

    @ram.cache(_forever_cache_key)
    def friendly_type2type_class(self, value):
        """Take the friendly type name and return a class for displaying
        the correct type.
        """
        proto_type = self.translate_friendly_type(value)
        return "type-%s" % proto_type

    @ram.cache(_forever_cache_key)
    def friendly_type2icon_class(self, value):
        """Take the friendly type name (e.g. OpenOffice Write Document)
        and return a class for displaying the correct icon.
        """
        proto_type = self.translate_friendly_type(value)
        if proto_type in ("odt", "word"):
            return "icon-doc-text"
        # files -> .icon-file-audio etc...
        if proto_type in (
            "archive",
            "audio",
            "code",
            "excel",
            "image",
            "pdf",
            "powerpoint",
            "video",
        ):
            return "icon-file-%s" % proto_type
        # known documents
        if proto_type in ("app", "email", "workspace", "folder", "link", "document"):
            return "icon-%s" % proto_type
        if proto_type == "rich":
            return "icon-doc-text"
        if proto_type == "people":
            return "icon-user"
        if proto_type == "file":
            return "icon-document"
        if proto_type == "event":
            return "icon-calendar"
        if proto_type == "task":
            return "icon-tasks"
        if "news" in proto_type:
            return "icon-newspaper"

        logger.warning("Cannot assign an icon class for: %s", value)
        return "icon-document"

    @memoize_contextless
    def translate_short_month_name(self, short_month_name, short=False):
        """Translate the short month name using plonelocales"""
        msg_id = f"month_{short_month_name}"
        if short:
            msg_id += "_abbr"
        return api.portal.translate(pl_message(msg_id))

    def date2month_name(self, date, short=False):
        """
        Return the full month name in the appropriate language
        """
        short_month_name = MONTH_TO_ABBR_MAP[date.month]
        return self.translate_short_month_name(short_month_name, short)

    @memoize_contextless
    def is_slow(self):
        """Check if should serve a version with reduced functionality"""
        ips = (
            (
                self.request.get("HTTP_X_FORWARDED_FOR")
                or self.request.get("REMOTE_ADDR", "")
            )
            .replace(",", " ")
            .split()
        )
        bad_ips = (
            api.portal.get_registry_record(
                "ploneintranet.layout.known_bad_ips", default=()
            )
            or ()
        )
        return not set(ips).isdisjoint(bad_ips)

    @ram.cache(_5min_cache_key)
    def get_avatar_tag(self, userid, link_to=None, link_class=None, ts=None):
        """Provide HTML tag to display the avatar
        The ts (timestamp) is optional and can be used to invaliate the cache
        """
        return pi_api.userprofile.avatar_tag(
            username=userid, link_to=link_to, link_class=link_class
        )

    @property
    @memoize_contextless
    def date_picker_options(self):
        """Return the date picker options"""
        # Plone assigns 0 to Monday, Pikaday 0 to Sunday :)
        first_day = first_weekday() + 1
        return ";".join(
            (
                "output-format: Do MMMM YYYY",
                f"locale: {self.language}",
                "i18n: {}/@@date-picker-i18n.json".format(
                    api.portal.get().absolute_url()
                ),
                f"first-day: {first_day}",
            )
        )

    @property
    @memoize
    def language(self):
        return api.portal.get_current_language()

    def pretty_time(self, dt, with_timezone=True):
        if not dt:
            return ""
        if with_timezone:
            return "{date} {tzname}".format(
                date=dt.strftime("%H:%M"), tzname=dt.tzinfo.tzname(dt)
            )
        else:
            return dt.strftime("%H:%M")

    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?
        Ref. duplicate: dashboard.py:276"""
        locale = self.request.locale
        formatter = locale.dates.getFormatter("date", length=length)
        if type(date) is DateTime:
            date = date.asdatetime()
        return formatter.format(date)

    def get_tag_classes(self, tags=[]):
        """Return classes based on tags"""
        classes = []
        for tag in tags:
            classes.append(f"tag-{idnormalizer.normalize(tag)}")
        return " ".join(classes)

    def min_plone_version(self, version):
        """Return True if the current Plone version is greater
        than or equal to the specified version."""
        current_version = packaging.version.parse(
            importlib.metadata.version("Products.CMFPlone")
        )
        parsed_version = packaging.version.parse(version)
        return current_version >= parsed_version

    @property
    @memoize_contextless
    @deprecate("No support anymore for this method, as we require Plone 6.1+")
    def min_plone52(self):
        """Return the Plone version."""
        return True

    def get_profile_by_id(self, userid):
        """return the user profile so that we can get the full name and url"""
        return pi_api.userprofile.get(userid)

    @property
    @ram.cache(_5min_cache_key)
    def brand_name(self):
        return api.portal.get_registry_record(
            "ploneintranet.theme.brand", default="quaive"
        )

    @property
    @ram.cache(_forever_cache_key)
    def logo_icon(self):
        """Build the url to the right brand logo"""
        return "{}/++theme++ploneintranet.theme/generated/assets/{}/favicon/touch-icon.png".format(  # noqa: E501
            api.portal.get().absolute_url(), self.brand_name
        )

    @property
    @ram.cache(_forever_cache_key)
    def email_logo_url(self) -> str:
        """Build the url to the right brand logo"""
        path = (
            f"++theme++ploneintranet.theme/generated/assets/"
            f"{self.brand_name}/mail/header.png"
        )
        portal = api.portal.get()
        try:
            resource = portal.restrictedTraverse(path)
        except Exception:
            return ""
        if not isinstance(resource, FilesystemFile):
            return ""
        return f"{portal.absolute_url()}/{path}"

    @memoize
    def redactor_options(self):
        """Return the options for pat-redactor in the format:

        key1: value1; key2: value2; ...
        """

        token = createToken()
        upload_target_url = parent_workspace(self.context)
        upload_target_url = upload_target_url or aq_parent(self.context)
        upload_target_url = upload_target_url.absolute_url()

        _options = {
            "toolbar-external": "#editor-toolbar",
            "buttons": "undo, redo, format, bold, italic, deleted, lists, link, line, image",  # noqa
            "formatting": "p, h1, h2, h3, blockquote, pre",
            "plugins": "alignment, table, video, imagemanager",
            "imageupload": "{url}/@@images-upload.json?_authenticator={token}".format(
                url=upload_target_url, token=token
            ),
            "imagegetjson": f"{upload_target_url}/@@images.json",
            "imageresizable": "true",
        }

        html_allowed_users = api.portal.get_registry_record(
            "ploneintranet.layout.editor_html_allowed_users"
        )
        try:
            if api.user.get_current().getUserName() in html_allowed_users:
                _options["show-source-button"] = "true"
                _options["buttons"] += ", html"
        except Exception:
            pass

        options = "; ".join(": ".join(option) for option in _options.items())
        return options

    @property
    @memoize_contextless
    def site_title(self):
        """Try to return a site title the Quaive way.
        If not possible try the Plone way
        """
        orga = api.portal.get_registry_record(
            "ploneintranet.layout.quaive_organisation", default=""
        )
        title = api.portal.get_registry_record(
            "ploneintranet.layout.quaive_title", default=""
        )
        limiter = orga and title and " " or ""
        return (
            "{}{}{}".format(orga or "", limiter, title or "")
            or api.portal.get_registry_record("plone.site_title", default="")
            or api.portal.get().Title()
        )

    @property
    @memoize_contextless
    def portal_url(self):
        return api.portal.get().absolute_url()

    @property
    @memoize_contextless
    def is_multilingual_enabled(self):
        if not api.portal.get_registry_record(
            "ploneintranet.layout.multilingual_enabled", default=False
        ):
            return False
        pl = api.portal.get_tool("portal_languages")
        return len(pl.settings.available_languages) > 1

    @property
    @memoize_contextless
    def show_osh_link(self):
        return api.portal.get_registry_record(
            "ploneintranet.layout.show_osh_link", default=True
        )

    def get_widget_classes(self, widget):
        """This is used by the component library to add classes to the widgets"""
        for classes in self.widget_class_mapping:
            if isinstance(widget, classes):
                return self.widget_class_mapping[classes]
        return ""

    def use_fieldset(self, widget):
        """Method that checks if a widget should be rendered within a fieldset"""
        return isinstance(widget, self.widgets_with_fieldset)

    def map_url(self, location, embed=False):
        """Return a google map link for a <string> location"""
        if not location:
            return ""
        params = {"q": location}
        if embed:
            params["output"] = "embed"
        return f"https://maps.google.com/maps?{urlencode(params)}"

    @property
    @memoize_contextless
    def is_mobile_android(self):
        """Check if the request is from an android mobile device"""
        user_agent = self.request.get("HTTP_USER_AGENT", "").lower()
        for mobile_signifier in [
            "android",
        ]:
            if mobile_signifier in user_agent:
                return True
        return False

    @property
    @memoize_contextless
    def is_mobile_ios(self):
        """Check if the request is from an iOS mobile device"""
        user_agent = self.request.get("HTTP_USER_AGENT", "").lower()
        for mobile_signifier in [
            "iphone",
            "ipad",
            "ipod",
        ]:
            if mobile_signifier in user_agent:
                return True
        return False

    @property
    @memoize_contextless
    def is_mobile_other(self):
        """Check if the request is from a mobile device other than android or iOS"""
        user_agent = self.request.get("HTTP_USER_AGENT", "").lower()
        for mobile_signifier in [
            "blackberry",
            "windows phone",
        ]:
            if mobile_signifier in user_agent:
                return True
        return False

    @property
    @memoize_contextless
    def jitsi_download_url(self):
        """Return the download URL for the Jitsi Meet client"""
        if self.is_mobile_android:
            return (
                "https://play.google.com/store/apps/details?"
                "id=org.jitsi.meet&hl=de&pli=1"
            )
        if self.is_mobile_ios:
            return "https://itunes.apple.com/us/app/jitsi-meet/id1165103905"
        # Fall back on the jitsi download page
        return "https://jitsi.org/downloads/"

    @property
    @memoize_contextless
    def is_mobile(self):
        """Check if the request is from a mobile device"""
        return self.is_mobile_android or self.is_mobile_ios or self.is_mobile_other

    def get_pat_autosuggest_options(self, widget) -> str:
        """Return the options for pat-autosuggest for the given widget

        The returned format is a JSON string that will look like:

        {
            "allow-new-words": "false",
            "max-selection-size": "0",
            "ajax-search-index": "text",
            "ajax-url": "http://example.com/vocabulary",
            "value-separator": ";",
            "prefill-json": "{\"value1\": \"Title 1\", \"value2\": \"Title 2\"}"
        }
        """
        # Take the options from the widget that work in plain Plone
        pattern_options = widget.get_pattern_options()
        separator = pattern_options.get("separator", ";")

        # Translate them into the options that pat-autosuggest expects
        ajax_url = pattern_options.get("vocabularyUrl")
        if ajax_url and "@@getVocabulary?" in ajax_url:
            ajax_url += "&resultsonly=1"
        options = {
            "allow-new-words": pattern_options.get("allowNewItems", "false"),
            "max-selection-size": pattern_options.get("maximumSelectionSize", "0"),
            "ajax-search-index": "text",
            "ajax-url": ajax_url,
            "value-separator": separator,
        }
        value = widget.value
        if value:
            try:
                # Try to compute the prefill_json,
                # which is a mapping of value to title for the current value(s)
                # of the widget.  Without prefill_json you only see the proper
                # selected items if the value and title of a term are the same.
                prefill_json = {}
                values = value.split(separator)
                if getattr(widget, "vocabulary"):
                    vocabulary_view = api.content.get_view(
                        "getVocabulary", self.context, self.request
                    )
                    vocabulary = vocabulary_view.get_vocabulary(
                        factory_name=widget.vocabulary,
                        field_name=widget.field and widget.field.__name__ or "",
                    )
                else:
                    source_view = api.content.get_view(
                        "getSource", widget, self.request
                    )
                    vocabulary = source_view.get_vocabulary()
                for element in values:
                    term = vocabulary.getTermByToken(element)
                    if term is not None:
                        prefill_json[term.value] = term.title
                # We need to pass the prefill_json as a string,
                # so that it can be converted to json again in the patternslib code
                # See https://github.com/Patternslib/Patterns/issues/1278
                options["prefill-json"] = json.dumps(prefill_json, indent=2)
            except Exception:
                logger.exception("Error while getting prefill_json for pat-autosuggest")

        return json.dumps(options, indent=2)
