import logging
from typing import TYPE_CHECKING

from ..utils import LazyProperty

if TYPE_CHECKING:  # pragma: no cover
    from .api import API

logger = logging.getLogger(__name__)


class Redirects:
    """
    General interface for working with MediaWiki's redirects.

    MediaWiki provides multiple different queries for handling redirects, but
    they are all kind of awkward:

    1. The `first way`_ using the ``redirects`` parameter assumes that you
       already know the source titles or pageids of the redirects you want to
       resolve. The relevant ``titles`` or ``pageids`` parameters have
       considerably lower limits compared to lists or generator modules.
    2. Second way uses `prop=redirects`_, which gives pages redirecting to
       pages generated by ``generator=allpages``. There are much higher limits,
       but all pages that are neither redirects or redirect targets are returned
       as well.
    3. It is also possible to use `generator=allpages`_ with the
       ``gapfilterredir=redirects`` parameter to generate only redirect pages,
       but it is not possible to get the redirect targets at the same time.
    4. There is also `list=allredirects`_ module, but its purpose is a great
       mystery to me.

    This class therefore uses the second method to query redirects for the whole
    wiki and caches the post-processed results. There are some limitations:

    - Interwiki redirects are not included in the mapping.

    .. _`first way`: https://www.mediawiki.org/wiki/API:Query#Resolving_redirects
    .. _`prop=redirects`: https://www.mediawiki.org/wiki/API:Redirects
    .. _`generator=allpages`: https://www.mediawiki.org/wiki/API:Allpages
    .. _`list=allredirects`: https://www.mediawiki.org/wiki/API:Allredirects
    """

    def __init__(self, api: "API"):
        self._api = api

    def fetch(
        self,
        source_namespaces: list[int] | str = "all",
        target_namespaces: list[int] | str = "all",
    ) -> dict[str, str]:
        """
        Build a mapping of redirects in given namespaces.

        Note that the mapping can contain double redirects, which could cause
        some algorithms to break. Always use :py:meth:`Redirects.resolve` to
        resolve redirects.

        :param list source_namespaces:
            the namespace ID of the source title must be in this list in order
            to be included in the mapping (default is ``[0]``, the magic word
            ``"all"`` will select all available namespaces)

            .. note::
                Due to a MediaWiki bug, this parameter does not have any effect.
                Should be fixed in 1.27: https://phabricator.wikimedia.org/T113453
        :param list target_namespaces:
            the namespace ID of the target title must be in this list in order
            to be included in the mapping (default is ``"all"``, which will
            select all available namespaces)
        :returns:
            a dictionary where the keys are source titles and values are the
            redirect targets, including the link fragments (e.g.
            ``"Page title#Section title"``).
        """
        if source_namespaces == "all":
            source_namespaces = [ns for ns in self._api.site.namespaces if int(ns) >= 0]
            source_namespaces_str = "*"
        else:
            source_namespaces_str = "|".join(str(ns) for ns in source_namespaces)
        if target_namespaces == "all":
            target_namespaces = [ns for ns in self._api.site.namespaces if int(ns) >= 0]

        redirects = {}
        for ns in target_namespaces:
            allpages = self._api.generator(
                generator="allpages",
                gapnamespace=ns,
                gaplimit="max",
                prop="redirects",
                rdprop="title|fragment",
                rdnamespace=source_namespaces_str,
                rdlimit="max",
            )
            for page in allpages:
                # construct the mapping, the query result is somewhat reversed...
                target_title = page["title"]
                for redirect in page.get("redirects", []):
                    source_title = redirect["title"]
                    target_fragment = redirect.get("fragment")
                    if target_fragment:
                        redirects[source_title] = "{}#{}".format(target_title, target_fragment)
                    else:
                        redirects[source_title] = target_title
        return redirects

    @LazyProperty
    def map(self) -> dict[str, str]:
        """
        A lazily evaluated mapping for all namespaces on the wiki.
        """
        return self.fetch()

    def resolve(self, source: str) -> str | None:
        """
        Looks into the :py:attr:`map` property and checks if given title is a
        redirect page. Double redirects are resolved repeatedly, if an infinite
        loop is detected, an error is logged and the page is treated as if it
        was not a redirect.

        :param str source: the title to be resolved
        :returns:
            A string of the last non-redirect target page if ``source`` is a
            redirect page, otherwise ``None``.
        """

        def _get(source: str | None, anchor: str | None) -> tuple[str | None, str | None]:
            target = self.map.get(source)
            if not target:
                return None, None
            try:
                target, anchor = target.split("#", maxsplit=1)
            except ValueError:
                pass
            return target, anchor

        intermediates = set()
        target, anchor = _get(source, None)
        while target in self.map and target not in intermediates:
            target, anchor = _get(target, anchor)
            intermediates.add(target)
        if target in self.map:
            logger.error(f"Failed to resolve last redirect target of '{source}': detected infinite loop.")
            return None
        if target and anchor:
            target += "#" + anchor
        return target
