from collections import defaultdict
from networkx.exception import NetworkXError
from plone import api
from plone.memoize.view import memoize_contextless
from ploneintranet import api as pi_api
from ploneintranet.notifications.interfaces import ICategory
from ploneintranet.notifications.interfaces import ICategoryProvider
from ploneintranet.notifications.interfaces import ILoggedEvent
from ploneintranet.workspace.config import INTRANET_USERS_GROUP_ID
from zope.component import adapter
from zope.interface import implementer
from zope.interface.interfaces import ComponentLookupError


@implementer(ICategory)
class BaseCategory:
    """Base class for all Category implementations that configure the core routing logic
    of which messages are sent via which channel to which recipients.

    The Category, for a specific logged_event (it behaves as an adapter on this)
    determines:
    1. which recipients should be notified of this logged_event
    2. for each recipient determines:
        - which message this recipient will receive
        - via which channels this recipient will receive that message

    Subclasses can configure default:

    - default channels
    - required channels
    - blocked channels

    via class attributes.

    The digest_daily and digest_weekly channels defaults can be switched on/off
    via registry record "ploneintranet.notifications.default_digest_frequency".

    User settings are hooked up to this category machinery to get the category
    name, title, help message, and default channels.
    """

    # We require the record channel for all categories
    # because we need to store the notification in the database
    # to be displayed in the notifications center
    required_channels = {"record"}
    # If you want to exclude a category from the digest, you can block it here.
    # E.g. a chat message that goes stale after time. Be sure to add both
    # "digest_daily" and "digest_weekly" in that case.
    blocked_channels = set()
    # You should never add digest_daily or digest_weekly to default channels,
    # anywhere. Instead, you can enable default sending of digest via registry
    # record "ploneintranet.notifications.default_digest_frequency".
    default_channels = set()

    name = ""
    # title and description should be translatable messages, and are shown in the
    # user settings UI.
    title = ""
    # description (optional) is shown as help text for a category in the settings UI.
    description = ""
    # _app_id is used to group categories per app
    _app_id = None

    highlighted_title_template = (
        """<strong style="font-weight: normal; color: darkgoldenrod;">"""
        """{title}"""
        """</strong>"""
    )

    def __init__(self, logged_event=None) -> None:
        self.logged_event = logged_event

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

    @classmethod
    def get_app(cls):
        if not cls._app_id:
            return None
        return pi_api.apps.get(cls._app_id, default=None)

    @property
    def recipient_ids(self):
        raise NotImplementedError

    def expand_principal_ids(self, principal_ids):
        """principal ids can be users or groups.
        We expand groups into user ids.
        """
        user_ids = set()
        pas_uids = set()
        pas_view = api.content.get_view("pas_view", api.portal.get())

        for principal_id in principal_ids:
            if principal_id != INTRANET_USERS_GROUP_ID:
                pas_uid = pas_view.principalname2uid(principal_id)
                if pas_uid.startswith("userprofile:"):
                    user_ids.add(principal_id)
                else:
                    try:
                        pas_uids.update(pas_view.descendants(pas_uid))
                    except KeyError:
                        # pas_uid is not in the graph
                        pass
                    except NetworkXError:
                        # The graph is missing a node
                        pass

        user_ids.update(
            pas_view.uids2principalnames(
                # exclude the group principals themselves
                [pas_uid for pas_uid in pas_uids if pas_uid.startswith("userprofile")]
            )
        )
        return user_ids

    @property
    def expanded_recipient_ids(self):
        """recipient ids can be users or groups.
        We expand groups into user ids.
        """
        return self.expand_principal_ids(self.recipient_ids)

    @property
    @memoize_contextless
    def all_intranet_user_ids(self):
        """Return all the user ids that are in the intranet group
        and that are enabled.
        """
        intranet_users = api.group.get(groupname=INTRANET_USERS_GROUP_ID)
        ids = intranet_users.getMemberIds()
        brains = pi_api.userprofile.get_users(
            exact_getUserId=ids, review_state="enabled", full_objects=False
        )
        return {brain.getUserId for brain in brains}

    def get_recipient_channels(self, recipient_id):
        """The channels for the given recipient_id are chosen by the user
        through its personal settings which are stored in the notification tool.

        If no choice was made we return the default channels for this category.

        If the user switched on the digest via her preferences, or the digest
        default frequency is configured via the portal registry, we enable the
        appropriate digest channel (digest_daily, or digest_weekly) on all
        categories.

        We have the possibility to set up required channels.
        We have the possibility to set up blocked channels, e.g. for excluding
        a category from the digests.
        """
        notifications_tool = api.portal.get_tool("ploneintranet_notifications")
        settings = notifications_tool._notification_settings.get(recipient_id, {})
        chosen_channels = set()  # avoid changing the class attribute
        chosen_channels |= settings.get(self.name, self.default_channels)
        default_frequency = (
            api.portal.get_registry_record(
                "ploneintranet.notifications.default_digest_frequency", default=""
            )
            or ""
        )  # make sure to cast None into ""
        digest_settings = notifications_tool._digest_settings.get(recipient_id, {})
        frequency = digest_settings.get("frequency", default_frequency)
        if frequency and frequency in ("daily", "weekly"):
            marker = object()
            digest_categories = digest_settings.get("categories", marker)
            if digest_categories is marker or self.name in digest_categories:
                chosen_channels.add(f"digest_{frequency}")
        return self.required_channels | chosen_channels - self.blocked_channels

    def get_message_for(self, notification, with_text=True, highlight=False):
        return "Not implemented message for notification"

    def maybe_process_title(self, title, highlight=False):
        """Shorten long titles, and wrap in color if requested"""
        if len(title) > 280:
            title = title[:280] + "…"
        if highlight:
            title = self.highlighted_title_template.format(title=title)
        return title

    def get_absolute_url_for(self, notification):
        context = notification.logged_event.context
        try:
            absolute_url = context.absolute_url()
        except AttributeError:
            portal_url = api.portal.get().absolute_url()
            return f"{portal_url}/apps/notifications"
        try:
            portal_type = context.portal_type
        except AttributeError:
            return absolute_url

        try:
            view_types = api.portal.get_registry_record(
                "plone.types_use_view_action_in_listings"
            )
        except ComponentLookupError:
            view_types = ()

        if portal_type in view_types:
            absolute_url += "/view"

        return absolute_url


@adapter(ILoggedEvent)
@implementer(ICategoryProvider)
class BaseCategoryProvider:
    """Base class for category providers per logged_event.

    The actual implementations live in microblog.notifications,
    news.notifications etc.
    """

    # The subclasses should provide the proper factories
    category_factories = []

    def __init__(self, logged_event) -> None:
        self.logged_event = logged_event

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

    @property
    def categories(self):
        """Build a list of categories for the logged_event"""
        return [factory(self.logged_event) for factory in self.category_factories]

    @property
    def recipients_channels(self):
        recipients_channels = defaultdict(dict)
        for category in self.categories:
            for user_id in category.expanded_recipient_ids:
                for channel in category.get_recipient_channels(user_id):
                    if not recipients_channels[user_id].get(channel):
                        recipients_channels[user_id][channel] = category
        return recipients_channels
