# -*- coding: utf-8; -*-
#
# This file is part of Superdesk.
#
# Copyright 2013, 2014 Sourcefabric z.u. and contributors.
#
# For the full copyright and license information, please see the
# AUTHORS and LICENSE files distributed with this source code, or
# at https://www.sourcefabric.org/superdesk/license

import re
import logging
from superdesk.io.registry import register_feed_parser
from superdesk.io.feed_parsers import XMLFeedParser
from superdesk.utc import utcnow
from email.utils import parsedate_to_datetime
from superdesk import etree as sd_etree
from superdesk.metadata.item import ITEM_TYPE, CONTENT_TYPE, GUID_FIELD
from superdesk.media.renditions import update_renditions
from xml.sax.saxutils import quoteattr
from lxml import etree
from collections import OrderedDict

logger = logging.getLogger(__name__)
nsmap = {
    "excerpt": "http://wordpress.org/export/1.2/excerpt/",
    "content": "http://purl.org/rss/1.0/modules/content/",
    "wfw": "http://wellformedweb.org/CommentAPI/",
    "dc": "http://purl.org/dc/elements/1.1/",
    "wp": "http://wordpress.org/export/1.2/",
}
embed_TPL = 'EMBED {} Image {{id: "{}"}}'

BLOCK_WHITELIST = {
    "<h",  # hr + h[1..9]
    "<br",
    "<p",
    "<div",
    "<iframe",
    "<ul",
    "<ol",
    "<li",
    "<table",
    "</",
}

SHORTCODE_REGEX = re.compile(r"\[/?[_a-z]+\]")


def is_block_elem(line):
    text = line.lstrip()
    for block in BLOCK_WHITELIST:
        if text.startswith(block):
            return True
    return False


def remove_shortcodes(text):
    return re.sub(SHORTCODE_REGEX, "", text)


class WPWXRFeedParser(XMLFeedParser):
    """
    Feed Parser for Wordpress' WXR format
    """

    NAME = "wpwxr"

    label = "Wordpress WXR parser"

    def __init__(self):
        super().__init__()
        # we use OrderedDict to have control on parsing order
        self.default_mapping = OrderedDict(
            [
                ("guid", "guid"),
                ("item_id", "guid"),
                ("_current_version", lambda _: 1),
                ("versioncreated", lambda _: utcnow()),
                ("firstpublished", {"xpath": "pubDate/text()", "filter": parsedate_to_datetime}),
                ("author", "dc:creator"),
                ("headline", "title"),
                # featuremedia must called before body_html, so thumbnail is used
                # in priority as feature media (else body_html would use first image
                # as feature media).
                ("featuremedia", self.parse_thumbnail),
                # images are handled in body_hook
                ("body_html", {"xpath": "content:encoded", "key_hook": self.body_hook}),
                ("keywords", {"xpath": 'category[@domain="post_tag"]', "list": True}),
                (
                    "anpa_category",
                    {
                        "xpath": 'category[@domain="category"]/text()',
                        "list": True,
                        "filter": lambda cat: {"qcode": cat, "name": cat},
                    },
                ),
                ("attachments", {"xpath": "wp:attachment_url", "list": True, "key_hook": self.attachments_hook}),
                ("original_article_url", {"xpath": "link", "default": "", "key_hook": self._set_original_article_url}),
            ]
        )

    def can_parse(self, xml):
        return xml.tag == "rss"

    def parse(self, xml, provider=None):
        return list(self.parse_items(xml))

    def parse_items(self, xml):
        for item_xml in xml.findall("channel/item"):
            try:
                post_type = item_xml.xpath("wp:post_type", namespaces=nsmap)[0].text
            except IndexError:
                pass
            else:
                if post_type and post_type != "post":
                    # we don't want to parse attachments
                    continue
            yield self.parse_item(item_xml)

    def parse_item(self, item_xml):
        item = {}
        self.do_mapping(item, item_xml, namespaces=nsmap)
        if "associations" in item:
            for _, data in item["associations"].items():
                if data is None:
                    continue
                # these 3 fields are mandatory in default setup
                # we use a space for that to avoid issue when publishing
                data.setdefault("headline", item["headline"])
                data.setdefault("alt_text", " ")
                data.setdefault("description_text", " ")
            if (
                len(item["associations"]) == 1
                and not item["body_html"]
                and item.get("associations", {}).get("featuremedia")
            ):
                # if the item only contains a feature media, we convert it to picture
                featuremedia = item["associations"]["featuremedia"]
                item["renditions"] = featuremedia["renditions"]
                item[ITEM_TYPE] = featuremedia[ITEM_TYPE]
                item["alt_text"] = item["headline"]
                item["media"] = item["renditions"]["original"]["media"]
                item["mimetype"] = item["renditions"]["original"]["mimetype"]
                del item["associations"]
        return item

    def check_url(self, url):
        """Check URL and add protocol in case of relative URL

        :param url: found URL
        :return unicode: HTTP(s) URL
        :raises ValueError: the URL is invlalid
        """
        if url is None:
            raise ValueError("No URL found")
        url = url.strip()
        if not url:
            raise ValueError("URL is empty")
        if url.startswith("//"):
            # if we have a protocol relative URL, we use https
            url = "https:" + url
        if not url.startswith("http"):
            raise ValueError("Url is not HTTP(s)")
        return url

    def _add_image(self, item, url):
        associations = item.setdefault("associations", {})
        association = {
            ITEM_TYPE: CONTENT_TYPE.PICTURE,
            "ingest_provider": self.NAME,
            GUID_FIELD: url,
        }
        update_renditions(association, url, None)

        # we use featuremedia for the first image, then embeddedX
        if "featuremedia" not in associations:
            key = "featuremedia"
        else:
            key = "embedded" + str(len(associations) - 1)

        associations[key] = association
        return key, association

    def parse_thumbnail(self, item_elt, item):
        """Check for _thumbnail_id meta_key, and use its attachment as feature media

        If the key is found, the linked item is looked for, and its attachment_url is used as feature media
        """
        thumbnail_elt = item_elt.xpath('wp:postmeta/wp:meta_key[text()="_thumbnail_id"]', namespaces=nsmap)
        if not thumbnail_elt:
            return
        thumbnail_elt = thumbnail_elt[0]

        try:
            post_id = thumbnail_elt.xpath("../wp:meta_value/text()", namespaces=nsmap)[0].strip()
            if not post_id:
                raise IndexError
        except IndexError:
            logger.warning(
                "invalid post_id, ignoring: {elt}".format(elt=sd_etree.to_string(thumbnail_elt.xpath("..")[0]))
            )
            return
        try:
            if '"' in post_id:
                raise ValueError('post id should not contain " (double quote)')
            post_id_elt = item_elt.xpath('/rss/channel/item/wp:post_id[text()="{}"]'.format(post_id), namespaces=nsmap)[
                0
            ]
            att_item_elt = post_id_elt.getparent()
            url = att_item_elt.xpath("wp:attachment_url", namespaces=nsmap)[0].text.strip()
            url = self.check_url(url)
        except (IndexError, ValueError) as e:
            logger.warning(
                "Can't find attachement URL, ignoring: {e}\n{elt}".format(
                    e=e, elt=sd_etree.to_string(thumbnail_elt.getparent())
                )
            )
            return
        try:
            key, media_data = self._add_image(item, url)
        except Exception as e:
            logger.error(e)
            return

        for key, elt_names in (("description_text", ("description", "title")), ("alt_text", ("title",))):
            for elt_name in elt_names:
                elt = att_item_elt.find(elt_name)
                if elt is not None and elt.text:
                    media_data[key] = elt.text
                    break
            else:
                media_data[key] = ""

    def body_hook(self, item, html):
        """Copy content to body_html

        if img are found in the content, they are uploaded.
        First image is used as feature media, then there are embeds
        """
        # we need to convert CRLF to <p>
        # cf. SDTS-22
        html = html.replace("&#13;", "\r")
        splitted = html.split("\r\n")
        if len(splitted) == 1 and "<p>" not in html:
            splitted = html.split("\n")
        if len(splitted) > 1:
            html = "".join(["<p>{}</p>".format(s) if not is_block_elem(s) else s for s in splitted if s.strip()])

        if "img" in html:
            content = sd_etree.parse_html(html, "html")
            for img in content.xpath("//img"):
                try:
                    src = self.check_url(img.get("src"))
                except ValueError:
                    logger.warning("Can't fetch image: {elt}".format(elt=sd_etree.to_string(img)))
                    continue
                try:
                    key, media_data = self._add_image(item, src)
                except Exception as e:
                    logger.error(e)
                    img.getparent().remove(img)
                    continue
                url = media_data["renditions"]["original"]["href"]
                img.set("src", url)
                if key == "featuremedia":
                    # no need to embed the image for featuremedia
                    continue
                embed_start = etree.Comment(embed_TPL.format("START", key))
                embed_end = etree.Comment(embed_TPL.format("END", key))
                img.addprevious(embed_start)
                img.addnext(embed_end)

            content = sd_etree.fix_html_void_elements(content)

            html = sd_etree.to_string(content, encoding="unicode", method="xml")

        html = remove_shortcodes(html)

        item["body_html"] = html

    def attachments_hook(self, item, attachments):
        """Attachment are parsed at the end

        if it's the first image found, it's used as feature media
        else it's used as embed and put at the end of body_html
        """
        for url in attachments:
            try:
                key, media_data = self._add_image(item, url)
            except Exception as e:
                logger.error(e)
                continue
            if key == "featuremedia":
                # no need to embed the image for featuremedia
                continue
            embed_start = "<!--" + embed_TPL.format("START", key) + "-->"
            embed_end = "<!--" + embed_TPL.format("END", key) + "-->"
            new_url = media_data["renditions"]["original"]["href"]
            img = '<img src={src} height="{height}" width="{width}">'.format(
                src=quoteattr(new_url),
                height=media_data["renditions"]["original"]["height"],
                width=media_data["renditions"]["original"]["width"],
            )
            item["body_html"] += "<div>" + embed_start + img + embed_end + "</div>"

    def _set_original_article_url(self, item, value):
        if value:
            item.setdefault("extra", {})["original_article_url"] = value


register_feed_parser(WPWXRFeedParser.NAME, WPWXRFeedParser())
