"""Deprecated bridge: Yodl sectref/subref → :zshsectref:`Title <manpage>`.

Resolve (section title, man page) using the same man→doc map as conf.py
man_pages. One build-finished summary warns with the remaining call-site
count (zsh.sectref-deprecated); list sites with
``rg ':zshsectref:' Doc/source``. Replace with :ref:/:doc: then delete
this file from extensions.

Silence the summary with: suppress_warnings = ["zsh.sectref-deprecated"]
"""

from __future__ import annotations

from typing import Any

from docutils import nodes
from docutils.parsers.rst import roles
from sphinx.application import Sphinx
from sphinx.util import logging
from sphinx.util.nodes import make_refnode, split_explicit_title

logger = logging.getLogger(__name__)

_SAME_DOC_PAGES = frozenset({"", "above", "below"})

# Count :zshsectref: uses during read; warn once at build-finished.
_SECTREF_USE_COUNT = 0


def _std_label(env, title: str):
    """Return (docname, target_id, sectname) from std domain, or None."""
    labels = env.domaindata.get("std", {}).get("labels", {})
    key = nodes.fully_normalize_name(title)
    if key in labels:
        return labels[key]
    if title in labels:
        return labels[title]
    want = title.casefold()
    for name, val in labels.items():
        if name.casefold() == want:
            return val
    return None


class zsh_sectref(nodes.Inline, nodes.TextElement):
    """Pending Yodl-style section reference (resolved in doctree-resolved)."""


def _man_to_doc(app: Sphinx) -> dict[str, str]:
    """Invert conf.py man_pages: manpage basename → docname."""
    mapping: dict[str, str] = {}
    for entry in app.config.man_pages or []:
        # (docname, name, desc, authors, section)
        if len(entry) < 2:
            continue
        docname, name = entry[0], entry[1]
        mapping[str(name)] = str(docname)
    return mapping


def _parse_role_text(text: str) -> tuple[str, str]:
    """Return (title, page) from role text; page may be empty."""
    text = nodes.unescape(text)
    has_explicit, title, target = split_explicit_title(text)
    if has_explicit:
        return title.strip(), target.strip()
    return text.strip(), ""


def _find_section_id(doctree: nodes.document, title: str) -> str | None:
    """Return first section id whose title matches (casefold), else None."""
    want = title.casefold()
    fold_id = None
    for section in doctree.findall(nodes.section):
        sect_title = ""
        for child in section.children:
            if isinstance(child, nodes.title):
                sect_title = child.astext().strip()
                break
        if not sect_title:
            continue
        ids = section.get("ids") or []
        if not ids:
            continue
        if sect_title == title:
            return ids[0]
        if fold_id is None and sect_title.casefold() == want:
            fold_id = ids[0]
    return fold_id


def zshsectref_role(
    typ: str,
    rawtext: str,
    text: str,
    lineno: int,
    inliner: Any,
    options: dict | None = None,
    content: list | None = None,
):
    global _SECTREF_USE_COUNT
    env = inliner.document.settings.env
    title, page = _parse_role_text(text)

    _SECTREF_USE_COUNT += 1

    node = zsh_sectref(rawtext, title)
    node["zshsectref_title"] = title
    node["zshsectref_page"] = page
    node["zshsectref_from"] = env.docname
    return [node], []


def _resolve_zsh_sectrefs(app: Sphinx, doctree: nodes.document, fromdocname: str) -> None:
    if not doctree.findall(zsh_sectref):
        return

    man_map = _man_to_doc(app)
    env = app.env

    for node in list(doctree.findall(zsh_sectref)):
        title = node["zshsectref_title"]
        page = node.get("zshsectref_page") or ""

        if page in _SAME_DOC_PAGES:
            # Prefer the doc where the role was parsed. Texinfo assembly can
            # re-resolve nodes with fromdocname set to the book root.
            todocname = node.get("zshsectref_from") or fromdocname
        else:
            todocname = man_map.get(page)
            if not todocname:
                logger.warning(
                    "zshsectref: unknown man page %r (title=%r)",
                    page,
                    title,
                    type="zsh",
                    subtype="sectref-unresolved",
                    location=node,
                )
                node.replace_self(nodes.emphasis(title, title))
                continue

        try:
            target_tree = env.get_doctree(todocname)
        except Exception:
            logger.warning(
                "zshsectref: document %r not in build (page=%r title=%r)",
                todocname,
                page or "(current)",
                title,
                type="zsh",
                subtype="sectref-unresolved",
                location=node,
            )
            node.replace_self(nodes.emphasis(title, title))
            continue

        target_id = _find_section_id(target_tree, title)
        if not target_id:
            # Explicit std labels (module assembler .. _Title:, or a
            # section that lives in a sibling doc such as Zsh/files while
            # the Yodl page arg still says zsh).
            label = _std_label(env, title)
            if label:
                todocname = label[0]
                target_id = label[1]

        if not target_id:
            logger.warning(
                "zshsectref: unresolved section %r in %r",
                title,
                todocname,
                type="zsh",
                subtype="sectref-unresolved",
                location=node,
            )
            node.replace_self(nodes.emphasis(title, title))
            continue

        contnode = nodes.Text(title)
        try:
            refnode = make_refnode(
                app.builder,
                fromdocname,
                todocname,
                target_id,
                contnode,
                title,
            )
        except Exception:
            # Builder may not support get_relative_uri yet for some outputs.
            refnode = nodes.reference(title, title, internal=True)
            refnode["refdocname"] = todocname
            refnode["refid"] = target_id
        node.replace_self(refnode)


def _warn_sectref_summary(app: Sphinx, exception: Exception | None) -> None:
    """One deprecation summary instead of a per-call WARNING flood."""
    if _SECTREF_USE_COUNT > 0 and exception is None:
        logger.warning(
            "zshsectref is deprecated: %d use(s) remain; "
            "list with rg \":zshsectref:\" source/ then replace with "
            ":ref: / :doc: and remove the zsh_sectref extension",
            _SECTREF_USE_COUNT,
            type="zsh",
            subtype="sectref-deprecated",
        )


def setup(app: Sphinx):
    app.add_node(zsh_sectref)
    roles.register_local_role("zshsectref", zshsectref_role)
    app.connect("doctree-resolved", _resolve_zsh_sectrefs)
    app.connect("build-finished", _warn_sectref_summary)
    return {
        "version": "1.0",
        "parallel_read_safe": True,
        "parallel_write_safe": True,
    }
