"""Markdown to HTML for editorial text, one renderer for the API and the server-rendered page.

Sources of truth: this module; `services/editorial.py` is the caller. The subset is deliberate:
headings, paragraphs, bulleted and numbered lists (one nested level), block quotes, rules,
fenced code, images on their own line, and inline bold, italic, code, links and images. It is
what `mammoth` emits from a Word document plus what a person types in a .md file; nothing
else is interpreted. Raw HTML in the source is text and is escaped, so the output is safe to
insert whatever the source, and a link or image is only rendered for `https://`, `http://`,
`mailto:` and site-relative addresses.

Why not a Markdown library: the article body must render identically in the SPA and in the
crawler-facing body Stream B stamps server-side, and the shortest way to guarantee that is one
function, called from both, whose output is plain semantic HTML with no options to drift. A
heading written as `#` in the body is rendered one level down (h2), because the article title
is the page's h1.
"""

from __future__ import annotations

import html
import re

_ESCAPABLE = set("\\`*_{}[]()#+-.!>|~")
_SAFE_HREF = re.compile(r"^(https?://|mailto:|/(?!/)|#)", re.I)
_BULLET = re.compile(r"^(\s*)([-*+])\s+(.*)$")
_NUMBERED = re.compile(r"^(\s*)(\d{1,3})[.)]\s+(.*)$")
_HEADING = re.compile(r"^(#{1,6})\s+(.*?)\s*#*\s*$")
_RULE = re.compile(r"^\s*([-*_])(\s*\1){2,}\s*$")
_IMAGE_LINE = re.compile(r"^!\[([^\]]*)\]\((\S*?)(?:\s+\"[^\"]*\")?\)\s*$")
_FENCE = re.compile(r"^```")
_AUTOLINK = re.compile(r"https?://[^\s<>\"']+")
_SLUG = re.compile(r"[^a-z0-9]+")


def heading_id(text: str) -> str:
    """A deep-link id for a heading: lowercase words joined by hyphens."""
    return _SLUG.sub("-", text.lower()).strip("-")[:80]


def safe_href(href: str) -> str | None:
    """The address if it is one we will link to, else None (the text still renders)."""
    href = href.strip()
    if not href or not _SAFE_HREF.match(href):
        return None
    return href


# --- inline ---------------------------------------------------------------------------------


def render_inline(text: str, *, in_link: bool = False) -> str:
    """Bold, italic, code, links, images and autolinks inside one block of text."""
    out: list[str] = []
    i, n = 0, len(text)
    while i < n:
        c = text[i]
        if c == "\\" and i + 1 < n and text[i + 1] in _ESCAPABLE:
            out.append(html.escape(text[i + 1]))
            i += 2
            continue
        if c == "`":
            end = text.find("`", i + 1)
            if end > i + 1:
                out.append(f"<code>{html.escape(text[i + 1 : end])}</code>")
                i = end + 1
                continue
        if c == "!" and text.startswith("![", i):
            parsed = _parse_bracketed(text, i + 1)
            if parsed is not None:
                alt, src, end = parsed
                href = safe_href(src)
                if href is not None:
                    out.append(
                        f'<img src="{html.escape(href, quote=True)}" alt="{html.escape(alt, quote=True)}" loading="lazy" />'
                    )
                else:
                    out.append(html.escape(alt))
                i = end
                continue
        if c == "[" and not in_link:
            parsed = _parse_bracketed(text, i)
            if parsed is not None:
                label, target, end = parsed
                href = safe_href(target)
                inner = render_inline(label, in_link=True)
                if href is not None:
                    rel = ' rel="noopener"' if href.lower().startswith("http") else ""
                    out.append(f'<a href="{html.escape(href, quote=True)}"{rel}>{inner}</a>')
                else:
                    out.append(inner)
                i = end
                continue
        if c in "*_":
            emphasised = _parse_emphasis(text, i, in_link)
            if emphasised is not None:
                markup, end = emphasised
                out.append(markup)
                i = end
                continue
        if (
            c == "h"
            and text.startswith(("http://", "https://"), i)
            and (i == 0 or not text[i - 1].isalnum())
        ):
            m = _AUTOLINK.match(text, i)
            if m:
                url = m.group(0).rstrip(".,;:!?)")
                out.append(
                    f'<a href="{html.escape(url, quote=True)}" rel="noopener">{html.escape(url)}</a>'
                )
                i += len(url)
                continue
        out.append(html.escape(c))
        i += 1
    return "".join(out)


def _parse_bracketed(text: str, start: int) -> tuple[str, str, int] | None:
    """`[label](target)` starting at `start` (which points at `[`). Returns (label, target, end)."""
    if start >= len(text) or text[start] != "[":
        return None
    depth, j = 0, start
    while j < len(text):
        if text[j] == "\\":
            j += 2
            continue
        if text[j] == "[":
            depth += 1
        elif text[j] == "]":
            depth -= 1
            if depth == 0:
                break
        j += 1
    if j >= len(text) or j + 1 >= len(text) or text[j + 1] != "(":
        return None
    # The address may itself hold balanced parentheses (Wikipedia-style URLs).
    depth, close = 0, j + 2
    while close < len(text):
        if text[close] == "(":
            depth += 1
        elif text[close] == ")":
            if depth == 0:
                break
            depth -= 1
        close += 1
    if close >= len(text):
        return None
    target = text[j + 2 : close].strip()
    # An optional "title" after the address is dropped.
    if " " in target:
        target = target.split(" ", 1)[0]
    return text[start + 1 : j], target, close + 1


def _parse_emphasis(text: str, i: int, in_link: bool) -> tuple[str, int] | None:
    c = text[i]
    if text.startswith(c * 3, i) and not text.startswith(c * 4, i):
        # ***both*** is strong and em together.
        end = text.find(c * 3, i + 3)
        if end > i + 3 and not text[i + 3].isspace() and not text[end - 1].isspace():
            inner = render_inline(text[i + 3 : end], in_link=in_link)
            return f"<strong><em>{inner}</em></strong>", end + 3
    for marker, tag in ((c * 2, "strong"), (c, "em")):
        if not text.startswith(marker, i):
            continue
        start = i + len(marker)
        if start >= len(text) or text[start].isspace():
            continue
        # Underscores inside a word (snake_case) are text, not emphasis.
        if c == "_" and i > 0 and (text[i - 1].isalnum()):
            continue
        end = start
        while True:
            end = text.find(marker, end)
            if end < 0:
                break
            after = end + len(marker)
            if text[end - 1].isspace() or text[end - 1] == "\\":
                end += 1
                continue
            if c == "_" and after < len(text) and text[after].isalnum():
                end += 1
                continue
            if (
                c == "*"
                and marker == "*"
                and text.startswith("**", end)
                and not text.startswith("***", end)
            ):
                # A lone * closing against a ** opener belongs to the strong span.
                end += 1
                continue
            break
        if end < 0 or end == start:
            continue
        inner = render_inline(text[start:end], in_link=in_link)
        return f"<{tag}>{inner}</{tag}>", end + len(marker)
    return None


# --- blocks ---------------------------------------------------------------------------------


def render_markdown(source: str) -> str:
    """The whole document as HTML. Empty input renders as an empty string."""
    lines = source.replace("\r\n", "\n").replace("\r", "\n").split("\n")
    out: list[str] = []
    i, n = 0, len(lines)
    while i < n:
        line = lines[i]
        if not line.strip():
            i += 1
            continue
        if _FENCE.match(line):
            j = i + 1
            while j < n and not _FENCE.match(lines[j]):
                j += 1
            code = "\n".join(lines[i + 1 : j])
            out.append(f"<pre><code>{html.escape(code)}</code></pre>")
            i = j + 1
            continue
        m = _HEADING.match(line)
        if m:
            level = min(max(len(m.group(1)), 2), 4)  # the title is the h1; nothing deeper than h4
            text = m.group(2)
            out.append(f'<h{level} id="{heading_id(text)}">{render_inline(text)}</h{level}>')
            i += 1
            continue
        if _RULE.match(line):
            out.append("<hr />")
            i += 1
            continue
        m = _IMAGE_LINE.match(line.strip())
        if m:
            href = safe_href(m.group(2))
            if href is not None:
                alt = html.escape(m.group(1), quote=True)
                caption = (
                    f"<figcaption>{render_inline(m.group(1))}</figcaption>"
                    if m.group(1).strip()
                    else ""
                )
                out.append(
                    f'<figure><img src="{html.escape(href, quote=True)}" alt="{alt}" loading="lazy" />{caption}</figure>'
                )
            i += 1
            continue
        if line.lstrip().startswith(">"):
            j = i
            quoted: list[str] = []
            while j < n and lines[j].strip() and lines[j].lstrip().startswith(">"):
                quoted.append(lines[j].lstrip()[1:].lstrip())
                j += 1
            out.append(f"<blockquote>{render_markdown(chr(10).join(quoted))}</blockquote>")
            i = j
            continue
        if _BULLET.match(line) or _NUMBERED.match(line):
            markup, i = _render_list(lines, i)
            out.append(markup)
            continue
        j = i
        para: list[str] = []
        while j < n and lines[j].strip() and not _is_block_start(lines[j]):
            para.append(lines[j])
            j += 1
        out.append(f"<p>{_join_paragraph(para)}</p>")
        i = j
    return "\n".join(out)


def _is_block_start(line: str) -> bool:
    return bool(
        _HEADING.match(line)
        or _RULE.match(line)
        or _FENCE.match(line)
        or _BULLET.match(line)
        or _NUMBERED.match(line)
        or line.lstrip().startswith(">")
        or _IMAGE_LINE.match(line.strip())
    )


def _join_paragraph(lines: list[str]) -> str:
    parts: list[str] = []
    for k, raw in enumerate(lines):
        text = raw.strip()
        hard_break = raw.endswith("  ") and k < len(lines) - 1
        parts.append(render_inline(text) + ("<br />" if hard_break else ""))
    return " ".join(p if p.endswith("<br />") else p for p in parts).replace("<br /> ", "<br />")


def _item(line: str) -> tuple[int, bool, str] | None:
    m = _BULLET.match(line)
    if m:
        return len(m.group(1).expandtabs(4)), False, m.group(3)
    m = _NUMBERED.match(line)
    if m:
        return len(m.group(1).expandtabs(4)), True, m.group(3)
    return None


def _render_list(lines: list[str], i: int) -> tuple[str, int]:
    """One list starting at `lines[i]`, with one level of nesting by indentation."""
    first = _item(lines[i])
    assert first is not None
    base_indent, ordered, _ = first
    tag = "ol" if ordered else "ul"
    items: list[str] = []
    n = len(lines)
    while i < n:
        item = _item(lines[i])
        if item is None or item[0] < base_indent or item[0] == base_indent and item[1] != ordered:
            break
        indent, _, text = item
        if indent > base_indent:
            nested, i = _render_list(lines, i)
            if items:
                items[-1] = items[-1][: -len("</li>")] + nested + "</li>"
            else:
                items.append(f"<li>{nested}</li>")
            continue
        i += 1
        # Continuation lines (indented, not an item) belong to the item.
        extra: list[str] = []
        while (
            i < n and lines[i].strip() and _item(lines[i]) is None and lines[i][:1] in (" ", "\t")
        ):
            extra.append(lines[i].strip())
            i += 1
        items.append(f"<li>{render_inline(' '.join([text.strip(), *extra]))}</li>")
    return f"<{tag}>{''.join(items)}</{tag}>", i


def plain_text(source: str, limit: int | None = None) -> str:
    """The document's words with no markup, for excerpts and descriptions."""
    text = re.sub(r"<[^>]+>", "", render_markdown(source))
    text = html.unescape(re.sub(r"\s+", " ", text)).strip()
    if limit is not None and len(text) > limit:
        cut = text[: limit - 1]
        if " " in cut:
            cut = cut[: cut.rfind(" ")]
        text = cut.rstrip(" ,;:") + "..."
    return text
