# -*- 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

from abc import ABCMeta, abstractmethod

from superdesk.etree import etree
from superdesk.errors import SkipValue
from flask import current_app as app
from superdesk.metadata.item import Priority
from collections import OrderedDict
import superdesk
import logging

logger = logging.getLogger(__name__)


class FeedParser(metaclass=ABCMeta):
    """
    Base class for a Feed Parser.

    A Feed Parser class must have the following attributes:
        1. `NAME` - unique name under which to register the class.
    """

    @abstractmethod
    def can_parse(self, article):
        """Sub-classes must override this method and tell whether it can parse the given article.

        :param article: article
        :return: True if the feed parser can parse, False otherwise.
        :rtype: bool
        """
        raise NotImplementedError()

    @abstractmethod
    def parse(self, article, provider=None):
        """Parse the given article and extracts the relevant elements/attributes values from the given article.

        :param article: XML String to parse
        :type article: str
        :param provider: Ingest Provider Details, defaults to None
        :type provider: dict having properties defined in
                        :py:class: `superdesk.io.ingest_provider_model.IngestProviderResource`
        :return: parsed data as dict.
        :rtype: dict having properties defined in :py:mod: `superdesk.metadata.item`
        """
        raise NotImplementedError()

    def set_dateline(self, item, city=None, text=None):
        """Sets the 'dateline' to the article identified by item.

        If city is passed then the system checks if city is available in Cities collection.
        If city is not found in Cities collection then dateline's located is set with default values.

        :param item: article.
        :type item: dict
        :param city: Name of the city, if passed the system will search in Cities collection.
        :type city: str
        :param text: dateline in full. For example, "STOCKHOLM, Aug 29, 2014"
        :type text: str
        """

        item.setdefault('dateline', {})

        if city:
            cities = app.locators.find_cities()
            located = [c for c in cities if c['city'] == city]
            item['dateline']['located'] = located[0] if len(located) > 0 else {'city_code': city, 'city': city,
                                                                               'tz': 'UTC', 'dateline': 'city'}
        if text:
            item['dateline']['text'] = text

    def map_priority(self, source_priority):
        """
        Maps the source priority to superdesk priority

        :param source_priority:
        :type source_priority: str

        :return: priority of the item
        :rtype int
        """

        if source_priority and source_priority.isdigit():
            if int(source_priority) in Priority.values():
                return int(source_priority)

        return Priority.Ordinary.value


class XMLFeedParser(FeedParser, metaclass=ABCMeta):
    """
    Base class for Feed Parsers which can parse the XML Content.
    """

    def __init__(self):
        self.root = None
        self.metadata_mapping = None

    def _parse_mapping(self, value):
        if isinstance(value, dict):
            if 'default_attr' in value:
                if 'default' in value:
                    logger.error("default and default_attr can't be used at the same time,"
                                 "only default will be used ({})".format(self.__class__))
                if 'xpath':
                    if '/' not in 'xpath':
                        logger.error("default_attr can be used for simple child element ({})".format(self.__class__))
                else:
                    logger.error("xpath is needed when default_attr is used ({})".format(self.__class__))
            if 'callback' in value and 'list' in value:
                del value['list']
                logger.error("list can't ve used with callback ({})".format(self.__class__))
            return value
        elif isinstance(value, str):
            if not value:
                return {}
            return {'xpath': value}
        elif callable(value):
            return {'callback': value}
        else:
            logger.warn("Can't parse mapping value {}, ignoring it".format(value))
            return {}

    def _generate_mapping(self, setting_param_name):
        """Generate self.metadata_mapping according to available mappings.

        The following mappings are used in this order (last is more important):
            - self.default_mapping
            - self.MAPPING, intended for subclasses
            - [setting_param_name] dictionary which can be put in settings
        If a value is a non-empty string, it is a xpath, @attribute can be used as last path component.
        If value is empty string/dict, the key will be ignored
        If value is a callable, it will be executed with nitf Element as argument, return value will be used.
        If a dictionary is used as value, following keys can be used:
            xpath: path to the element
            callback: callback executed with nitf Element as argument, return value will be used
            default: value to use if element/attribute doesn't exists (default: doesn't set the key)
            list: a bool which indicate if a list is expected
                  if False (default), only first value is used
            filter: callable to be used with found element/value
            default_attr: value if element exist but attribute is missing
                this works actually for all values, if it is not found parent element is checked
                and default_attr is used only if parent element exists
            key_hook: a callable which store itself the resulting value in the item,
                      usefull for specific behaviours when several values goes to same key
                      callable will get item and value as arguments.
            update: a bool which indicate that default mapping must be updated instead of overwritten
        Note the difference between using a callable directly, and "filter" in a dict:
        the former get the root element and can be skipped with SkipValue, while the
        later get an element/value found with xpath.
        """
        try:
            class_mapping = self.MAPPING
        except AttributeError:
            class_mapping = {}

        if setting_param_name is not None:
            settings_mapping = getattr(superdesk.config, setting_param_name)
            if settings_mapping is None:
                logging.info("No mapping found in settings for NITF parser, using default one")
                settings_mapping = {}
        else:
            settings_mapping = {}

        mapping = self.metadata_mapping = OrderedDict()

        for source_mapping in (self.default_mapping, class_mapping, settings_mapping):
            for key, value in source_mapping.items():
                key_mapping = self._parse_mapping(value)
                if key_mapping.get('update', False) and key in mapping:
                    mapping[key].update(key_mapping)
                else:
                    mapping[key] = key_mapping

    def do_mapping(self, item, item_xml, setting_param_name=None, namespaces=None):
        """Apply mapping to item's XML content to get article metadata

        mapping is generated by self._generate_mapping
        :param item: dictionary to fill with item metadata
        :type item: dict
        :param item_xml: XML element to parse
        :type item_xml: lxml.etree.Element
        :param setting_param_name: name of the settings attribute containing the mapping
        :type setting_param_name: str
        :type setting_param_name: NoneType
        :param namespaces: namespaces map to use with lxml methods
        :type namespaces: dict
        :type namespaces: NoneType
        """
        if self.metadata_mapping is None:
            self._generate_mapping(setting_param_name)

        for key, mapping in self.metadata_mapping.items():
            if not mapping:
                # key is ignored
                continue
            try:
                xpath = mapping['xpath']
            except KeyError:
                # no xpath, we must have a callable
                try:
                    values = [mapping['callback'](item_xml)]
                except KeyError:
                    logging.warn("invalid mapping for key {}, ignoring it".format(key))
                    continue
                except SkipValue:
                    continue
                list_ = False
            else:
                values = item_xml.xpath(xpath, namespaces=namespaces)
                list_ = mapping.get('list', False)
                if not list_:
                    values = values[:1]
                if not values:
                    # nothing found, we check default
                    try:
                        values = [mapping['default']]
                    except KeyError:
                        if 'default_attr' in mapping:
                            parent = item_xml.xpath(xpath[:xpath.rfind('/')], namespaces=namespaces)
                            if parent:
                                # default_attr is only used when there is a parent element
                                values = [mapping['default_attr']]
                            else:
                                continue
                        else:
                            # if there is not default value we skip the key
                            continue
                else:
                    for idx, current_value in enumerate(values):
                        if isinstance(current_value, str):
                            if 'filter' in mapping:
                                values[idx] = mapping['filter'](current_value)
                        else:
                            # we have an element,
                            # do we want a filter or the content?
                            try:
                                # filter
                                values[idx] = mapping['filter'](current_value)
                            except KeyError:
                                # content
                                values[idx] = ''.join(current_value.itertext())

            value = values if list_ else values[0]
            if 'key_hook' in mapping:
                mapping['key_hook'](item, value)
            else:
                item[key] = value

        return item

    def qname(self, tag, ns=None):
        """
        Return the Qualified Name of given XML tag.

        :param tag: XML Tag
        :type tag: str
        :param ns: Namespace to be used for generating Qualified Name, defaults to None.
        :type ns: str
        :return: Qualified Name of tag
        :rtype: str
        """

        if ns is None:
            ns = self.root.tag.rsplit('}')[0].lstrip('{')
        elif ns is not None and ns == 'xml':
            ns = 'http://www.w3.org/XML/1998/namespace'

        return str(etree.QName(ns, tag))


class FileFeedParser(FeedParser, metaclass=ABCMeta):
    """
    Base class for Feed Parsers which can parse the content in a file.
    """

    pass


class EmailFeedParser(FeedParser, metaclass=ABCMeta):
    """
    Base class for Feed Parsers which can parse email message.
    """

    pass


# must be imported for registration
from superdesk.io.feed_parsers.anpa import ANPAFeedParser  # NOQA
from superdesk.io.feed_parsers.iptc7901 import IPTC7901FeedParser  # NOQA
from superdesk.io.feed_parsers.newsml_1_2 import NewsMLOneFeedParser  # NOQA
from superdesk.io.feed_parsers.newsml_2_0 import NewsMLTwoFeedParser  # NOQA
from superdesk.io.feed_parsers.nitf import NITFFeedParser  # NOQA
from superdesk.io.feed_parsers.rfc822 import EMailRFC822FeedParser  # NOQA
from superdesk.io.feed_parsers.wenn_parser import WENNFeedParser  # NOQA
from superdesk.io.feed_parsers.dpa_iptc7901 import DPAIPTC7901FeedParser  # NOQA
from superdesk.io.feed_parsers.afp_newsml_1_2 import AFPNewsMLOneFeedParser  # NOQA
from superdesk.io.feed_parsers.scoop_newsml_2_0 import ScoopNewsMLTwoFeedParser  # NOQA
from superdesk.io.feed_parsers.ap_anpa import AP_ANPAFeedParser  # NOQA
from superdesk.io.feed_parsers.pa_nitf import PAFeedParser  # NOQA
from superdesk.io.feed_parsers.efe_nitf import EFEFeedParser  # NOQA
from superdesk.io.feed_parsers.wordpress_wxr import WPWXRFeedParser  # NOQA
from superdesk.io.feed_parsers.ninjs import NINJSFeedParser  # NOQA