"""
Utilities for processing MediaWiki pages into a more usable form.

Notes:\n
  - :mod:`wikitextparser` handles lists, but does not handle stripping of formatting around strings
  - :mod:`mwparserfromhell` does not handle lists, but does handle stripping of formatting around strings
    - Replaced the need for this module for now by implementing :func:`strip_style<.utils.strip_style>`

:author: Doug Skrypa
"""

import logging
from functools import cached_property, reduce
from operator import xor
from typing import TYPE_CHECKING, Optional, Union, Iterable, Mapping

from wikitextparser import WikiText

from .nodes import Root, Template, String, CompoundNode, Tag, Link

if TYPE_CHECKING:
    from .http import MediaWikiClient

__all__ = ['WikiPage']
log = logging.getLogger(__name__)


class WikiPage(Root):
    _ignore_category_prefixes = ()

    def __init__(
        self,
        title: str,
        site: Optional[str],
        content: Union[str, WikiText],
        categories: Iterable[str],
        preserve_comments: bool = False,
        interwiki_map: Mapping[str, str] = None,
        client: 'MediaWikiClient' = None,
    ):
        """
        :param title: The page title
        :param site: The site of origin for this page
        :param content: The page content
        :param categories: This page's categories
        :param preserve_comments: Whether HTML comments should be dropped or included in parsed nodes
        :param interwiki_map: Mapping of interwiki link prefix to wiki URL
        :param client: The MediaWikiClient from which this page originated
        """
        super().__init__(content, site, preserve_comments=preserve_comments, interwiki_map=interwiki_map)
        self.title = title
        self._categories = categories
        self._client = client

    def __repr__(self) -> str:
        return f'<{type(self).__name__}[{self.title!r} @ {self.site}]>'

    @cached_property
    def _sort_key(self):
        return self.is_disambiguation, self.title, self.site

    def __eq__(self, other: 'WikiPage') -> bool:
        if not isinstance(other, WikiPage):
            return False
        return self._sort_key == other._sort_key

    def __hash__(self) -> int:
        return reduce(xor, map(hash, (self.__class__, self.site, self.title, self.raw.string)))

    def __lt__(self, other: 'WikiPage') -> bool:
        return self._sort_key < other._sort_key

    @cached_property
    def categories(self) -> set[str]:
        """The lower-case categories for this page, with ignored prefixes (if applicable) filtered out"""
        categories = {
            cat for cat in map(str.lower, self._categories) if not cat.startswith(self._ignore_category_prefixes)
        }
        return categories

    @cached_property
    def similar_name_link(self) -> Optional[Link]:
        content = self.sections.content
        if content.__class__ is CompoundNode and isinstance(content[0], Template) and content[0].name == 'about':
            return Link.from_title(content[0][2].value, self)
        return None

    @cached_property
    def disambiguation_link(self) -> Optional[Link]:
        if any('articles needing clarification' in cat for cat in self.categories):
            return Link.from_title(f'{self.title}_(disambiguation)', self)
        return None

    @cached_property
    def is_disambiguation(self) -> bool:
        return any('disambiguation' in cat for cat in self.categories)

    @cached_property
    def is_template(self) -> bool:
        return self.title.startswith('Template:')

    @cached_property
    def as_link(self) -> Link:
        return Link.from_title(self.title, self)

    @cached_property
    def url(self) -> str:
        if self._client is not None:
            return self._client.url_for_article(self.title)
        raise AttributeError(f'Unable to determine URL when not initialized via MediaWikiClient')

    @cached_property
    def infobox(self) -> Optional[Template]:
        """
        Turns the infobox into a dict.  Values are returned as :class:`WikiText<wikitextparser.WikiText>` to allow for
        further processing of links or other data structures.  Wiki lists are converted to Python lists of WikiText
        values.
        """
        section_0_content = self.sections.content
        if isinstance(section_0_content, Template) and 'infobox' in section_0_content.name.lower():
            return section_0_content
        elif isinstance(section_0_content, CompoundNode):
            try:
                for node in self.sections.content:
                    if isinstance(node, Template) and 'infobox' in node.name.lower():
                        return node
            except Exception as e:
                log.log(9, f'Error iterating over first section content of {self}: {e}')
        return None

    def intro(self, strip_refs: bool = False) -> Union[String, CompoundNode, None]:
        """
        Neither parser provides access to the 1st paragraph directly when an infobox template precedes it - need to
        remove the infobox from the 1st section, or any other similar elements.
        """
        error = False
        intro = None
        try:
            # for i, node in enumerate(self.sections.content):
            for node in self.sections.content:
                # log.info(f'Processing node#{i}: {node.__class__.__name__}')
                if isinstance(node, String) and not node.value.startswith('{{DISPLAYTITLE:'):
                    # log.debug(f'Found intro in node#{i}')
                    intro = node
                    break
                elif isinstance(node, Tag) and node.name == 'div' and type(node.value) is CompoundNode:
                    # log.debug(f'Found intro in node#{i}')
                    intro = node.value
                    break
                elif isinstance(node, Tag) and node.name == 'p' and node.attrs.get('id') == 'firstHeading':
                    intro = node.value
                    break
                elif type(node) is CompoundNode and allowed_in_intro(node):
                    # log.debug(f'Found intro in node#{i}')
                    intro = node
                    break
                # else:
                #     log.debug(f'The intro is not node#{i} - it is a {node.__class__.__name__}')
                #     if type(node) is CompoundNode:
                #         import json
                #         from collections import Counter
                #         types = Counter(n.__class__.__name__ for n in node)
                #         log.debug(f' > Node contents: {json.dumps(types, sort_keys=True, indent=4)}')
        except Exception:
            error = True
            log.log(9, f'Error iterating over first section content of {self}:', exc_info=True)

        if intro is None and not error and self.infobox:
            found_infobox = False
            try:
                for node in self.sections.content:
                    if node is self.infobox:
                        found_infobox = True
                    elif found_infobox and type(node) is CompoundNode and starts_with_basic(node):
                        intro = node
                        break
            except Exception:
                log.log(9, f'Error iterating over first section content of {self}:', exc_info=True)

        if strip_refs and intro.__class__ is CompoundNode:
            nodes = [node for node in intro if not (isinstance(node, Tag) and node.name == 'ref')]
            if nodes != intro.children:
                log.debug(f'Removed {len(intro.children) - len(nodes)} ref nodes from intro for {self}')
                intro.children = nodes

        return intro

    def links(self, unique: bool = True, special: bool = False, interwiki: bool = False) -> set[Link]:
        """
        :param unique: Only include links with unique titles
        :param special: Include special (file, image, category, etc) links
        :param interwiki: Include interwiki links
        :return: The set of Link objects matching the specified filters
        """
        links = {ln for ln in self.find_all(Link) if (special or not ln.special) and (interwiki or not ln.interwiki)}
        return set({link.title: link for link in links}.values()) if unique else links


def allowed_in_intro(node: CompoundNode) -> bool:
    return node.only_basic or all(n.is_basic or (isinstance(n, Tag) and n.name == 'ref') for n in node)


def starts_with_basic(node: CompoundNode) -> bool:
    if first := next(iter(node), None):
        return first.is_basic
    return False
