from Acquisition import aq_chain
from collective.mustread.interfaces import ITracker
from collective.mustread.models import MustRead
from DateTime import DateTime
from plone import api
from plone.app.event.base import localized_now
from plone.memoize.view import memoize
from plone.memoize.view import memoize_contextless
from ploneintranet import api as pi_api
from ploneintranet.asynctasks.tasks import MarkRead
from ploneintranet.calendar.content import ICentralCalendar
from ploneintranet.core import _
from ploneintranet.core.i18nl10n import ulocalized_time
from ploneintranet.layout.app import IApp
from ploneintranet.layout.interfaces import IAppContent
from ploneintranet.layout.interfaces import IDiazoDashboardTemplate
from ploneintranet.layout.utils import shorten
from ploneintranet.news.browser.utils import obj2dict
from ploneintranet.news.content import INewsApp
from ploneintranet.news.content import NewsApp
from ploneintranet.news.interfaces import IMagazineHome
from ploneintranet.search.interfaces import ISiteSearch
from Products.Five.browser import BrowserView
from sqlalchemy.exc import OperationalError
from zope.component import getUtility
from zope.deprecation import deprecate
from zope.interface import implementer

import datetime
import logging

log = logging.getLogger(__name__)


class BaseView(BrowserView):
    @property
    @memoize_contextless
    def user(self):
        return api.user.get_current()

    @memoize_contextless
    def is_member_of_group(self, groupid):
        """Check if the current user is a direct member of the group
        specified by groupid
        """
        if not groupid:
            return True

        userid = self.user.getId()
        mt = api.portal.get_tool("membrane_tool")
        for brain in mt(exact_getGroupId=groupid):
            obj = brain.getObject()
            if userid in getattr(obj, "members", set()):
                return True
        return False

    @memoize
    def sections(self, show="all"):
        sections = []
        for section in self.app.sections():
            css_class = ""
            nav = section.navigation
            if show != "all":
                if nav == "exclude":
                    continue
                if section.nav_visible_group_list and not any(
                    map(self.is_member_of_group, section.nav_visible_group_list)
                ):
                    continue
            if show == "visible" and nav != "main":
                continue
            if show == "invisible" and nav != "other":
                continue
            if section in self.context.aq_chain:
                css_class = css_class + " current"
            sections.append(
                obj2dict(section, "absolute_url", "title", "id", css_class=css_class)
            )
        return sections

    @property
    @memoize
    def app(self) -> NewsApp | None:
        """Return containing workspace
        Returns None if not found.
        """
        for parent in aq_chain(self.context):
            if IApp.providedBy(parent):
                return parent

    def get_scale(self, grid_type, number):
        if grid_type == "1":
            return "3col"
        if grid_type == "2":
            return "2col"
        if grid_type == "3":
            return "1col"
        if grid_type == "1-3":
            if number == 1:
                return "3col"
            else:
                return "1col"
        if grid_type == "2-3":
            if number <= 2:
                return "2col"
            else:
                return "3col"
        return "3col"


class SubNav(BaseView):
    def sections(self, show="all"):
        _sections = super().sections(show=show)
        if show != "invisible":
            css_class = self.context.UID() == self.app.UID() and "current " or ""
            frontpage = obj2dict(
                self.app, "absolute_url", "id", css_class=css_class + "icon-home"
            )
            frontpage["title"] = _("Front page")
            _sections.insert(0, frontpage)

        return _sections

    @property
    def in_other_section(self):
        return "current" in {
            cls.strip()
            for section in self.sections("invisible")
            for cls in section["css_class"].split(" ")
        }

    def __call__(self):
        return self.index()


@implementer(IDiazoDashboardTemplate, IAppContent)
class NewsMagazine(BaseView):
    app_name = "news"
    section_class = "section-news"

    section = None
    show_sections_navigation = True

    visible_columns_by_grid = {
        "sl": ["1", "2"],  # Small Left Column
        "sr": ["2", "3"],  # Small Right Column
        "sc": ["2"],  # Single Column
        "tc": ["1", "2", "3"],  # Three Columns
    }

    @property
    @memoize
    def publisher_view(self):
        """Return the publisher view in the context of the app"""
        return api.content.get_view("publisher", self.app, self.request)

    @property
    @memoize
    def sections_by_uid(self):
        columns = self.context.columns
        uids = []
        for column in columns:
            for uid in columns[column]:
                uids.append(uid)
        pc = api.portal.get_tool("portal_catalog")
        brains = pc.searchResults(UID=uids)
        return {brain.UID: brain.getObject() for brain in brains}

    def get_obj(self, uid=None):
        return self.sections_by_uid.get(uid)

    @memoize
    def get_syndication_view(self, section):
        """Check if we have a smart section
        that wants to return news items from a view
        """
        view_name = getattr(section, "view_name", "")
        if not view_name:
            # Handle the legacy case where we have a logical syndication,
            # i.e. when the feed_url is a zope view
            # See also: test_views.MagazineTest.test_trending_section
            view_name = getattr(section, "feed_url", "")
            if not view_name:
                return
            if not view_name.startswith("@@"):
                # The feed URL is not a syndication view
                return
        if view_name.startswith("@@"):
            view_name = view_name[2:]
        return api.content.get_view(view_name, section, self.request)

    @memoize
    def _news_items(self, section=None, start=None, limit=None):
        """Get the news items in a section
        Section can be either an object or a UID
        """
        if section is None:
            return []

        syndication_view = self.get_syndication_view(section)
        if syndication_view:
            return syndication_view.news_items()

        now = datetime.datetime.now(datetime.UTC)

        # Check the language that we will use to serve the news
        profile = pi_api.userprofile.get_current()
        language = api.portal.get_current_language()
        if profile:
            language = (
                profile.preferred_content_language or profile.language or language
            )

        query = {
            "-review_state": self.publisher_view.unpublished_states,
            "expires__gt": now,
            "effective__lt": now,
            "languages": language,
        }

        # On the news landing page, only show "magazine home" news items.
        if INewsApp.providedBy(self.context):
            query["object_provides"] = [IMagazineHome.__identifier__]

        result = self.app.news_items(
            section, start=start, limit=limit, pinned=True, **query
        )
        return result

    def news_items(self, section=None, start=None, limit=None):
        return self._news_items(section=section, start=start, limit=limit)

    @memoize
    def trending_items(self, limit=None):
        tracker = getUtility(ITracker)
        most_read_query = tracker.get_most_read_query(days=14)
        try:
            # raises when the we cannot perform the query
            if not most_read_query.count():
                return []
        except OperationalError:  # db error. happens in tests.
            log.error("Cannot retrieve trending items. Skipping.")
            return []

        # do not show 4 topmost magazine items twice
        leading_uids = [brain.UID for brain in self.app.news_items(limit=4)]
        most_read_query_without_leading = most_read_query.filter(
            MustRead.uid.notin_(leading_uids),
        )

        if not most_read_query_without_leading.count():
            return []
        uids = [row.uid for row in most_read_query_without_leading]
        brains = sorted(
            (
                brain
                for brain in self.app.news_items(
                    UID=uids,
                    limit=limit,
                )
            ),
            key=lambda brain: uids.index(brain.UID),
        )
        return [
            obj2dict(brain.getObject(), counter=idx) for idx, brain in enumerate(brains)
        ]

    def trending_top5(self):
        return self.trending_items(5)

    def trending_hasmore(self):
        # return bool(self.trending_items()[5:6])
        # design is incomplete, not implemented
        return False

    @memoize
    def allow_commenting(self):
        return api.portal.get_registry_record(
            "ploneintranet.news.allow_commenting", default=False
        )

    def get_extra_class(self, item):
        return "event-card" if item.portal_type == "Event" else ""


class NewsSectionView(NewsMagazine):
    @property
    @memoize
    def section(self):
        return self.context

    def news_items(self):
        return self._news_items(
            section=self.context,
            start=self.b_start,
            limit=self.b_size,
        )

    # Batching support

    @property
    def view_name(self):
        return self.__name__

    @property
    def b_size(self):
        return int(self.request.get("b_size", 6))

    @property
    def b_start(self):
        return int(self.request.get("b_start", 0))

    @property
    def has_more(self) -> bool:
        """Check if the query has more results after the current batch"""
        news_items = self.news_items()
        try:
            count = news_items.total_results
        except AttributeError:
            count = len(news_items)
        return count > (self.b_start + self.b_size)

    @deprecate("Deprecated in 6.3.2.dev0. Use news_items instead.")
    def batch(self):
        syndication_view = self.get_syndication_view(self.context)
        if syndication_view and syndication_view.batch.__code__ != self.batch.__code__:
            return syndication_view.news_items()
        return self.news_items()

    @property
    def show_title(self):
        """The title of the News Magazine view is only shown if the section is
        not already obvious via a highlighted pill"""
        return self.section in [
            section["obj"] for section in self.sections("invisible")
        ]


class TrendingNewsSectionView(NewsSectionView):
    @deprecate("Deprecated in 6.3.2.dev0. Use news_items instead.")
    def batch(self):
        return self.news_items()

    def news_items(self):
        items = sorted(
            self.trending_items(), key=lambda item: item.get("counter"), reverse=True
        )
        return [it["obj"] for it in items]


class MyGroupsNewsSectionView(NewsSectionView):
    def news_items(self):
        sections = self.context.aq_parent.listFolderContents()

        user = api.user.get_current()
        pg = api.portal.get_tool("portal_groups")
        my_groupids = pg.getGroupsForPrincipal(user)

        if not my_groupids:
            return []

        sections = [
            section
            for section in sections
            if set(getattr(section, "nav_visible_group_list", set())) & set(my_groupids)
        ]
        if not sections:
            return []

        now = datetime.datetime.now(datetime.UTC)
        query = {
            "path": ["/".join(section.getPhysicalPath()) for section in sections],
            "-review_state": self.publisher_view.unpublished_states,
            "expires__gt": now,
            "effective__lt": now,
        }

        return self.app.news_items(
            start=self.b_start, limit=self.b_size, pinned=True, **query
        )


class EventsNewsSectionView(NewsSectionView):
    def central_calendar_uids(self):
        calendars = api.portal.get_tool("portal_catalog")(
            object_provides=ICentralCalendar.__identifier__
        )
        return [cal.UID for cal in calendars]

    def news_items(self):
        now = localized_now()
        query = dict(portal_type="Event", start__gt=now)
        calendar = getattr(self.context, "calendar", None)
        if calendar:
            query["shared_calendar"] = calendar
        else:
            query["shared_calendar"] = self.central_calendar_uids()
        sitesearch = getUtility(ISiteSearch)
        brains = sitesearch.query(
            filters=query, start=self.b_start, step=self.b_size, sort="start"
        )
        remainder = max(0, len(brains) - (self.b_start + self.b_size))
        items = (
            [None] * self.b_start
            + [brain.getObject() for brain in brains]
            + [None] * remainder
        )
        return items


class FeedItem(BaseView):
    @property
    @memoize
    def portal_url(self):
        return api.portal.get().absolute_url()

    def can_edit(self):
        return api.user.has_permission("Modify portal content", obj=self.context)

    def description(self, desc_len=160):
        return shorten(self.context.description, desc_len)

    def date(self):
        return ulocalized_time(
            self.context.effective(),
            long_format=1,
            formatstring_domain="ploneintranet",
            context=self.context,
        )

    def category(self):
        return self.context.aq_parent.title

    @property
    def magazine_image_object(self):
        """Return the object that contains the image for the magazine view

        It can be the news itself or a carousel slide
        """
        if self.context.image:
            return self.context
        for relation in self.context.carousel_slides:
            obj = relation.to_object
            if getattr(obj, "image", None):
                return obj

    def get_img_style(self, scale: str = "mini") -> str | None:
        """Return the style for the image tag"""
        obj = self.magazine_image_object
        if not obj:
            return
        images_view = api.content.get_view("images", obj, self.request)
        scale = images_view.scale(fieldname="image", scale=scale)
        if scale is None:
            return
        try:
            return f"background-image: url({scale.url}"
        except Exception:
            log.exception("Error getting image url")
            return

    def render(
        self, scale="mini", hide_category=True, show_descriptions=True, show_images=True
    ):
        """Call the index passing the scale as an option"""
        return self.index(
            scale=scale,
            hide_category=hide_category,
            show_descriptions=show_descriptions,
            show_images=show_images,
        )

    @memoize
    def number_statusupdates(self):
        if not api.portal.get_registry_record(
            "ploneintranet.news.allow_commenting", default=False
        ):
            return 0
        container = pi_api.microblog.get_microblog()
        if container is None:
            return 0
        counter = 0
        for status_update in container.content_values(self.context):
            if status_update.thread_id:
                counter += 1
        return counter


class NewsItemView(NewsMagazine):
    @property
    @memoize
    def section(self):
        return self.context.aq_parent

    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 isinstance(date, DateTime):
            date = date.asdatetime()
        return formatter.format(date)

    @property
    @memoize
    def author_id(self):
        return self.context.Creator()

    @property
    @memoize
    def author(self):
        return pi_api.userprofile.get(self.author_id)

    @property
    @memoize
    def author_name(self):
        if self.author:
            return self.author.fullname
        else:
            return self.author_id

    @property
    @memoize
    def author_url(self):
        return f"{self.author.absolute_url()}#main" if self.author else ""

    def date(self):
        return self.format_date(self.context.effective())

    @property
    def description(self):
        """Return the description if it was not auto-generated from the body text."""
        if self.context.description != getattr(self.context, "_teaser", ""):
            return self.context.description
        return ""

    @property
    def hero_image_aspect_ratio(self):
        """Translate dev naming cropped|uncropped into proto css naming auto|full
        and take the sitewide default into consideration.
        """
        selected_value = (
            self.context.hero_image_aspect_ratio
            or api.portal.get_registry_record(
                "ploneintranet.news.hero_default_aspect_ratio", default="cropped"
            )
        )
        if selected_value == "uncropped":
            return "full"
        return "auto"

    def __call__(self):
        MarkRead(self.context, self.request)()
        return super().__call__()
