#! python3  # noqa: E265


"""RSS reader logic."""

# ############################################################################
# ########## Imports ###############
# ##################################

# Standard library
import xml.etree.ElementTree as ET
from email.utils import parsedate
from functools import partial
from typing import TYPE_CHECKING

# QGIS
from qgis.core import Qgis
from qgis.PyQt.QtCore import QUrl

# project
from oslandia.__about__ import __title__, __version__
from oslandia.constants import UTM_CONTENT_NEWSFEED
from oslandia.newsfeed.mdl_rss_item import RssItem
from oslandia.toolbelt import PlgLogger, PlgOptionsManager
from oslandia.toolbelt.file_stats import is_file_older_than
from oslandia.toolbelt.network_manager import NetworkRequestsManager
from oslandia.toolbelt.qt_utils import TranslatableMixin, get_locale


if TYPE_CHECKING:
    from pathlib import Path

    from oslandia.toolbelt.preferences import PlgSettingsStructure


# ############################################################################
# ########## Classes ###############
# ##################################


class RssReader(TranslatableMixin):
    """Minimalist RSS feed parser."""

    FEED_ITEMS: list[RssItem] | None = None
    HEADERS: dict = {
        b"Accept": b"application/xml",
        b"User-Agent": bytes(f"{__title__}/{__version__}", "utf8"),
    }
    XML_NAMESPACES: dict[str, str] = {
        "atom": "http://www.w3.org/2005/Atom",
        "content": "http://purl.org/rss/1.0/modules/content/",
        "dc": "http://purl.org/dc/elements/1.1/",
        "slash": "http://purl.org/rss/1.0/modules/slash/",
        "sy": "http://purl.org/rss/1.0/modules/syndication/",
        "wfw": "http://wellformedweb.org/CommentAPI/",
    }

    def __init__(self) -> None:
        """Class initialization."""
        self.log = PlgLogger().log
        self.plg_settings: PlgSettingsStructure = PlgOptionsManager.get_plg_settings()
        self.ntwk_manager = NetworkRequestsManager()

        feed_file_name = "rss-fr.xml"
        if get_locale().name() != "fr_FR":
            feed_file_name = "rss-us.xml"
        self.local_feed_filepath: Path = self.plg_settings.local_app_folder.joinpath(
            "newsfeed", feed_file_name
        )

    def process(self) -> None:
        """Download, parse and read RSS feed than store items as attribute."""
        # download remote RSS feed to cache folder
        self.download_feed()
        if not self.local_feed_filepath.exists():
            self.log(
                message=self.tr(
                    "The RSS feed is not available locally. "
                    "Disabling RSS reader related features."
                ),
                log_level=Qgis.MessageLevel.Warning,
            )
            return

        # parse the local RSS feed
        self.read_rss()

        # check if a new item has been published since last check
        if not self.has_new_content:
            self.log(
                message="No new item found in RSS feed.",
                log_level=Qgis.MessageLevel.NoLevel,
            )
            return
        # notify
        if isinstance(self.latest_item, RssItem):
            latest_item = self.latest_item

            display_notif = True
            if self.plg_settings.rss_category_filters != "":
                display_notif = False
                user_filters: list[str] = [
                    filter_cat.strip()
                    for filter_cat in self.plg_settings.rss_category_filters.split(",")
                ]
                lowercase_user_filters: list[str] = [
                    cat.lower() for cat in user_filters
                ]
                lowercase_last_item_categories: list[str] = [
                    cat.lower() for cat in latest_item.categories
                ]
                common_categories: set[str] = set(lowercase_last_item_categories) & set(
                    lowercase_user_filters
                )
                if common_categories:
                    display_notif = True

            if display_notif:
                self.log(
                    message="{} {}".format(
                        self.tr("New content published:"),
                        latest_item.title,
                    ),
                    log_level=Qgis.MessageLevel.Success,
                    push=self.plg_settings.rss_notify_push_info,
                    duration=self.plg_settings.rss_notify_push_duration,
                    button=True,
                    button_text=self.tr("Read more..."),
                    button_connect=partial(self.on_read_item, latest_item),
                )

    def download_feed(self) -> bool:
        """Download RSS feed locally if it's older than latest 24 hours.

        :return: True is a new file has been downloaded.
        :rtype: bool
        """
        if is_file_older_than(
            local_file_path=self.local_feed_filepath,
            expiration_rotating_hours=self.plg_settings.rss_poll_frequency_hours,
        ):
            self.ntwk_manager.download_file_to(
                remote_url=self.plg_settings.rss_source,
                local_path=self.local_feed_filepath,
                caller=UTM_CONTENT_NEWSFEED,
            )
            self.log(
                message=f"The remote RSS feed ({self.plg_settings.rss_source}) has been "
                f"downloaded to {self.local_feed_filepath}",
                log_level=Qgis.MessageLevel.Info,
            )
            return True
        self.log(
            message=f"A fresh local RSS feed already exists: {self.local_feed_filepath}",
            log_level=Qgis.MessageLevel.Info,
        )
        return False

    def on_read_item(self, rss_item: RssItem):
        """Slot ran when end-user want to a read an item.

        :param rss_item: RSS item.
        :type rss_item: RssItem
        """
        self.ntwk_manager.open_url(QUrl(rss_item.url), UTM_CONTENT_NEWSFEED)

        self.plg_settings.rss_latest_content_guid = rss_item.guid
        guid_viewed = self.plg_settings.rss_read_items.split(",")
        if rss_item.guid not in guid_viewed:
            guid_viewed.append(rss_item.guid)
            self.plg_settings.rss_read_items = ",".join(guid_viewed)

        PlgOptionsManager.save_from_object(self.plg_settings)

    def is_read(self, rss_item: RssItem) -> bool:
        """Check if an item has been read.

        :param rss_item: RSS item.
        :type rss_item: RssItem

        :return: Boolean (true the item has been read, false else))
        :rtype: bool
        """

        guid_viewed = self.plg_settings.rss_read_items.split(",")
        return rss_item.guid in guid_viewed

    def is_category_in_filters(self, category: str) -> bool:
        """Check if a tag is in the user filters.

        :param category: Item category.
        :type category: str

        :return: Boolean (true the category is in the filters, false else))
        :rtype: bool
        """
        lowercase_user_filters = self.plg_settings.get_filters_list(lower=True)
        if category.lower() in lowercase_user_filters:
            return True

        return False

    def read_rss(self, no_filter: bool = False) -> list[RssItem]:
        """Parse the feed XML as string and store items into an ordered list of RSS items.

        :param no_filter: Tells if we want to filter or not
        :type no_filter: bool

        :return: list of RSS items dataclasses
        :rtype: list[RssItem]
        """

        if not self.local_feed_filepath.exists():
            self.process()

        # We need to reload settings in case they have changed
        self.plg_settings = PlgOptionsManager.get_plg_settings()
        feed_items: list[RssItem] = []
        tree: ET.ElementTree[ET.Element[str]] = ET.parse(self.local_feed_filepath)
        lowercase_user_filters: list[str] = self.plg_settings.get_filters_list(
            lower=True
        )

        items = tree.findall(path="channel/item", namespaces=self.XML_NAMESPACES)
        for item in items:
            try:
                # parse item and load it as object
                categories: list[str] = [
                    category.text for category in item.findall("category")
                ]
                feed_item_obj = RssItem(
                    abstract=item.find("description").text,
                    authors=[
                        author.text
                        for author in item.findall(
                            "creator", namespaces=self.XML_NAMESPACES
                        )
                    ]
                    or None,
                    categories=categories or None,
                    date_pub=parsedate(item.find("pubDate").text),
                    guid=item.find("guid").text,
                    title=item.find("title").text,
                    url=item.find("link").text,
                )
                if item.find("enclosure") is not None:
                    item_enclosure = item.find("enclosure")
                    feed_item_obj.image_length = item_enclosure.attrib.get("length")
                    feed_item_obj.image_type = item_enclosure.attrib.get("type")
                    feed_item_obj.image_url = item_enclosure.attrib.get("url")

                # If a filter has been set, filter the feed
                item_filtered = True
                if self.plg_settings.rss_category_filters != "":
                    item_filtered = False
                    lowercase_categories: list[str] = [
                        cat.lower() for cat in categories
                    ]
                    common_categories: set[str] = set(lowercase_categories) & set(
                        lowercase_user_filters
                    )
                    if common_categories:
                        item_filtered = True

                if (
                    feed_item_obj.abstract
                    == "Sorry, this entry is only available in French."
                ):
                    item_filtered = False

                # add items to the feed
                if item_filtered or no_filter:
                    feed_items.append(feed_item_obj)
            except Exception as err:
                item_idx: int | None = None
                if hasattr(items, "index"):
                    item_idx = items.index(item)

                err_msg: str = f"Feed item {item_idx} triggers an error. Trace: {err}"
                self.log(message=err_msg, log_level=Qgis.MessageLevel.Critical)

        # store feed items as attribute and return it
        self.FEED_ITEMS = feed_items
        return feed_items

    @property
    def latest_item(self) -> RssItem | None:
        """Returns the latest feed item, based on index 0.

        :return: latest feed item.
        :rtype: RssItem
        """
        if not self.FEED_ITEMS:
            self.log(
                message=self.tr(
                    "Feed has not been loaded, so it's impossible to "
                    "return the latest item."
                ),
                log_level=Qgis.MessageLevel.Warning,
            )
            return None

        return self.FEED_ITEMS[0]

    def latest_items(self, count: int = 10) -> list[RssItem]:
        """Returns the latest feed items.

        :param count: number of items to fetch
        :type count: int

        :return: latest feed items
        :rtype: List[RssItem]
        """
        if count <= 0:
            raise ValueError("Number of RSS items to get must be > 0")
        if not self.FEED_ITEMS:
            self.log(
                message=self.tr(
                    "Feed has not been loaded, so it's impossible to "
                    "return the latest item."
                ),
                log_level=Qgis.MessageLevel.Warning,
            )
            return []
        return self.FEED_ITEMS[:count]

    @property
    def has_new_content(self) -> bool:
        """Compare the saved item guid (in plugin settings) with feed latest item to \
        determine if a newer item has been published.

        :return: True is a newer item has been published.
        :rtype: bool
        """
        settings: PlgSettingsStructure = PlgOptionsManager.get_plg_settings()
        if (
            isinstance(self.latest_item, RssItem)
            and self.latest_item.guid != settings.rss_latest_content_guid
        ):
            return True
        else:
            return False
