from BTrees.OOBTree import OOBTree
from collections import defaultdict
from plone import api
from plone.memoize.view import memoize_contextless
from ploneintranet import api as pi_api
from ploneintranet.core import _
from ploneintranet.layout.browser.base import BaseView
from ploneintranet.layout.interfaces import IAppView
from ploneintranet.messaging.notifications.adapters import ChatMessagesCategory
from ploneintranet.microblog.notifications.adapters import (
    StatusUpdateActivityCommentsCategory,
)
from ploneintranet.microblog.notifications.adapters import (
    StatusUpdateActivityPostsCategory,
)
from ploneintranet.microblog.notifications.adapters import (
    StatusUpdateInteractionsCategory,
)
from ploneintranet.network.notifications.adapters import BaseFollowingCategory
from ploneintranet.news.content import INewsSection
from ploneintranet.news.notifications.adapters import NewsPublishedCategory
from ploneintranet.todo.notifications.adapters import BaseTodoUpdatedCategory
from ploneintranet.workspace.basecontent.utils import dexterity_update
from ploneintranet.workspace.notifications.adapters import BaseWorkspaceMemberCategory
from ploneintranet.workspace.notifications.adapters import BaseWorkspaceOpenCategory
from urllib.parse import parse_qs
from urllib.parse import urlencode
from urllib.parse import urlparse
from urllib.parse import urlunparse
from zope.annotation import IAnnotations
from zope.interface import implementer

import logging

log = logging.getLogger(__name__)


@implementer(IAppView)
class PersonalSettings(BaseView):
    """User personal settings"""

    app_name = "preferences"
    title = _("label_title_app_preferences", default="Preferences")
    news_tile_sections_key = "ploneintranet.layout.news_tile_sections"

    # To add a new notification category to the personal settings, all you have to
    # do is add it in categories. The rest plugs itself.
    # TODO generalize this into an adapter lookup that is truly pluggable.
    categories = (
        StatusUpdateInteractionsCategory,
        BaseFollowingCategory,
        StatusUpdateActivityPostsCategory,
        StatusUpdateActivityCommentsCategory,
        NewsPublishedCategory,
        BaseWorkspaceMemberCategory,
        BaseWorkspaceOpenCategory,
        BaseTodoUpdatedCategory,
        ChatMessagesCategory,
    )

    _frequencies = {
        "": _("label_never", default="Never"),
        "daily": _("label_daily", default="Daily"),
        "weekly": _("label_weekly", default="Weekly"),
    }

    @property
    def notification_channels(self):
        """Return the available notification channels for the UI.

        Note that even when channels are disabled, we still store
        the default preferences in the backend. See below.
        """
        channels = {}
        if self.desktop_notifications_enabled:
            channels["desktop"] = _("Desktop notifications")
        if self.email_notifications_enabled:
            channels["email"] = _("E-mail")
        return channels

    def maybe_disable_diazo(self):
        """Disable diazo if needed"""
        return False

    @property
    def apps(self):
        return [None] + pi_api.apps.get_apps()

    @property
    def grouped_categories(self):
        grouped = defaultdict(list)
        for category in self.categories:
            grouped[category.get_app()].append(category)
        return grouped

    @property
    def portal_languages(self):
        return api.portal.get_tool("portal_languages")

    @property
    def desktop_notifications_enabled(self):
        return api.portal.get_registry_record(
            "ploneintranet.notifications.desktop.enabled", default=False
        )

    @property
    def email_notifications_enabled(self):
        return api.portal.get_registry_record(
            "ploneintranet.notifications.email.enabled", default=True
        )

    @property
    def notifications_enabled(self):
        return self.desktop_notifications_enabled or self.email_notifications_enabled

    @property
    @memoize_contextless
    def userprofile(self):
        return pi_api.userprofile.get_current()

    @property
    @memoize_contextless
    def annotations(self):
        """Return the current authenticated user annotations
        or a dict if they cannot be retrieved
        """
        return IAnnotations(self.userprofile, {})

    @property
    @memoize_contextless
    def has_selected_news_section_uids(self):
        """Check if the user has done some selection of the news section"""
        return self.news_tile_sections_key in self.annotations

    @property
    @memoize_contextless
    def selected_news_section_uids(self):
        """Return the list of the selected options"""
        return self.annotations.get(self.news_tile_sections_key, [])

    @property
    @memoize_contextless
    def news_section_options(self):
        """Look for all the news section that we have"""
        brains = api.content.find(
            object_provides=INewsSection, sort_on="getObjPositionInParent"
        )
        nodata = not self.has_selected_news_section_uids
        if nodata:
            data = []
        else:
            data = self.selected_news_section_uids
        return [
            {
                "checked": "checked" if nodata or brain.UID in data else None,
                "title": brain.Title,
                "value": brain.UID,
            }
            for brain in brains
        ]

    @property
    def notifications_settings(self):
        notifications_tool = api.portal.get_tool("ploneintranet_notifications")
        try:
            userid = self.userprofile.getId()
        except AttributeError:
            return {}
        settings = notifications_tool._notification_settings
        if userid not in settings:
            if self.request.method == "POST":
                settings[userid] = OOBTree()
            else:
                return {}
        return settings[userid]

    def selected_channels(self, category):
        channels = self.notifications_settings.get(
            category.name,
            category.default_channels,
        )
        return channels

    def category_options(self, category):
        settings = self.selected_channels(category)
        return [
            {
                "value": channel,
                "label": api.portal.translate(self.notification_channels.get(channel)),
                "checked": channel in settings,
            }
            for channel in self.notification_channels
        ]

    @memoize_contextless
    def category_config(self, category):
        return {
            "label": api.portal.translate(category.title),
            "message": api.portal.translate(category.description),
            "options": self.category_options(category),
        }

    @property
    def digest_settings(self):
        notifications_tool = api.portal.get_tool("ploneintranet_notifications")
        try:
            userid = self.userprofile.getId()
        except AttributeError:
            return {}
        settings = notifications_tool._digest_settings
        if userid not in settings:
            if self.request.method == "POST":
                settings[userid] = OOBTree()
            else:
                return {}
        return settings[userid]

    @property
    def selected_digest_frequency(self):
        default_frequency = (
            api.portal.get_registry_record(
                "ploneintranet.notifications.default_digest_frequency", default=""
            )
            or ""
        )  # make sure None is cast to ""
        return self.digest_settings.get("frequency", default_frequency)

    @property
    def digest_frequency_options(self) -> list[dict]:
        """Return a list of dicts with:
        - a label
        - selected state
        - value
        """
        options = [
            {
                "label": api.portal.translate(label),
                "value": key,
                "selected": (
                    "selected" if key == self.selected_digest_frequency else None
                ),
            }
            for key, label in self._frequencies.items()
        ]
        return options

    @property
    def digest_channel_options(self) -> list[dict]:
        """Return a list of dicts with:
        - a label
        - checked state
        - value
        """
        marker = list()
        enabled = self.digest_settings.get("categories", marker)
        options = []
        for category in self.categories:
            # You can mark a channel as not being supported in a digest
            # E.g. chat messages which are stale when the digest gest sent.
            if f"digest_{self.selected_digest_frequency}" in category.blocked_channels:
                continue
            value = category.name
            # All channels are latently active by default and become really
            # activated when the digest frequency is set to "daily" or "weekly",
            # either via the UI settings, or via the registry configured
            # default_digest_frequency.
            if value in enabled or enabled is marker:
                checked = "checked"
            else:
                checked = None

            options.append(
                {
                    "label": api.portal.translate(category.title),
                    "value": value,
                    "checked": checked,
                }
            )

        return options

    def is_browser_negotiation_active(self):
        return api.portal.get_registry_record(
            "plone.use_request_negotiation", default=False
        )

    def get_browser_requested_language(self):
        """Try to get the language this page was requested with

        The header, if present might look like this: 'it,en-US;q=0.7,en;q=0.3'
        """
        return self.request.get("HTTP_ACCEPT_LANGUAGE", "").partition(",")[0]

    def _edit_notifications_settings(self):
        """Update the notifications settings if needed.

        If channels are disabled site-wide, we hide them from the UI but add
        them back into storage here.

        - As long as the killswitch for a channel is active, the backend will
          prevent any notifications from going out on that channel

        - When the killswitch is removed, we want all users to revert to the
          default settings for that channel, even if they saved non-default
          settings for other channels.
        """
        settings = self.notifications_settings
        for category in self.categories:
            category_settings = settings.get(category.name, category.default_channels)
            selected = set(self.request.get(f"notifications_{category.name}", []))
            selected.remove("MARKER")
            if (
                "desktop" in category_settings
                and not self.desktop_notifications_enabled
            ):
                selected.add("desktop")
            if "email" in category_settings and not self.email_notifications_enabled:
                selected.add("email")
            if category_settings != selected:
                settings[category.name] = selected

    def _edit_digest_settings(self):
        """Update the digest settings if needed"""
        settings = self.digest_settings
        old = settings.get("frequency")
        new = self.request.form.get("digest_frequency", "")

        if old != new:
            settings["frequency"] = new

        old = settings.get("categories", [])
        new = sorted(self.request.form.get("digest_categories", []))
        if old != new:
            settings["categories"] = new

    def edit(self):
        # dashboard news
        annotations = self.annotations
        new_uids = self.request.get("news_section_uid", [])
        old_uids = annotations.get(self.news_tile_sections_key, [])
        if new_uids != old_uids:
            self.annotations[self.news_tile_sections_key] = new_uids

        if self.notifications_enabled:
            self._edit_notifications_settings()

        if self.email_notifications_enabled:
            self._edit_digest_settings()

        modified, errors = dexterity_update(self.userprofile, self.request)
        if modified and "language" in set.union(*modified.values()):
            language = self.userprofile.language

            if not language:
                self.request.response.cookies.pop("I18N_LANGUAGE", None)
                if self.is_browser_negotiation_active():
                    language = self.get_browser_requested_language()
            else:
                self.portal_languages.setLanguageCookie(language, request=self.request)

            if not language:
                language = self.portal_languages.getDefaultLanguage()
            parsed = urlparse(self.request.getURL())
            query = parse_qs(parsed.query)
            query["set_language"] = language
            new_url = urlunparse(parsed[:4] + (urlencode(query),) + parsed[5:])
            self.request.RESPONSE.redirect(new_url)

        if errors:
            api.portal.show_message(
                _("There was a problem: ${errors}.", mapping={"errors": errors}),
                request=self.request,
                type="error",
            )

    def back_url(self):
        return f"{api.portal.get().absolute_url()}/apps#portal-content"

    @property
    def default_language_name(self):
        return api.portal.translate(
            self.portal_languages.getNameForLanguageCode(
                self.portal_languages.getDefaultLanguage()
            ),
            domain="ploneintranet",
        )

    @property
    def supported_languages(self):
        available = self.portal_languages.getAvailableLanguageInformation()
        langs = [
            available[lang] for lang in self.portal_languages.getSupportedLanguages()
        ]
        return langs

    def __call__(self):
        """Maybe edit before returning the template"""
        if self.request.get("form.buttons.edit"):
            self.edit()
        return super().__call__()
