#!/usr/bin/env python3
"""md_prescan.py — deterministic pre-scan for markdown-render pipelines.

Scans RENDERED HTML for the corruption signatures of the "inline-pass
sequencing" bug class: a renderer that runs link/emphasis/autolink
replacements sequentially over one string lets a later pass match inside
markup an earlier pass already emitted. Observable artifacts:

  nested-anchor   <a> element opened while still inside an <a> (browsers
                  break the outer anchor open; markup leaks as text)
  attr-markup     a literal '<tag' sequence inside a quoted attribute
                  value — emphasis or autolink fired inside alt=/href=/src=
  attr-backtick   a backtick inside href/src — a code span got swallowed
                  into the address by a bare-URL pass
  sentinel-leak   a \\x00 byte in the output — a code/escape sentinel the
                  renderer failed to restore (or an autolink pass consumed
                  part of one, leaving the residue behind)
  orphan-em       <em>/<strong>/<code> element whose only parent chain is
                  an attribute context — covered by attr-markup; kept as a
                  separate rule for unquoted-attribute edge cases

Usage:
  python3 md_prescan.py < rendered.html
  python3 md_prescan.py file.html [file2.html ...]
  python3 md_prescan.py --selftest

Output: JSON {"ok": bool, "findings": [{type, line, col, snippet, detail}]}
Exit 0 when clean, 1 when findings exist, 2 on usage error.
Stdlib only. Deterministic — no network, no model calls.
"""

import json
import re
import sys
from html.parser import HTMLParser

MARKUP_TAGS = ("em", "strong", "a", "img", "code", "b", "i", "del", "mark")


class Scanner(HTMLParser):
    def __init__(self):
        super().__init__(convert_charrefs=False)
        self.findings = []
        self.anchor_depth = 0

    def _add(self, ftype, detail, snippet=""):
        line, col = self.getpos()
        self.findings.append({
            "type": ftype,
            "line": line,
            "col": col,
            "snippet": snippet[:160],
            "detail": detail,
        })

    def handle_starttag(self, tag, attrs):
        if tag == "a":
            if self.anchor_depth > 0:
                self._add("nested-anchor",
                          "<a> opened inside an open <a> element — a later "
                          "inline pass linkified text inside an emitted anchor",
                          self.get_starttag_text() or "")
            self.anchor_depth += 1
        for name, val in attrs:
            if val is None:
                continue
            hit = re.search(r"<\s*(%s)\b" % "|".join(MARKUP_TAGS), val)
            if hit:
                self._add("attr-markup",
                          "attribute %s= contains literal <%s — an inline "
                          "pass emitted markup inside an emitted attribute"
                          % (name, hit.group(1)),
                          self.get_starttag_text() or "")
            if "\x00" in val:
                self._add("sentinel-leak",
                          "attribute %s= contains a \\x00 sentinel byte" % name,
                          self.get_starttag_text() or "")
            if name in ("href", "src") and "`" in val:
                self._add("attr-backtick",
                          "%s= contains a backtick — a code span was probably "
                          "swallowed into the address by a bare-URL pass"
                          % name,
                          self.get_starttag_text() or "")

    def handle_startendtag(self, tag, attrs):
        self.handle_starttag(tag, attrs)
        if tag == "a":
            self.anchor_depth -= 1

    def handle_endtag(self, tag):
        if tag == "a" and self.anchor_depth > 0:
            self.anchor_depth -= 1

    def handle_data(self, data):
        if "\x00" in data:
            self._add("sentinel-leak",
                      "\\x00 sentinel byte leaked into rendered text",
                      data)


def scan(html_text):
    """Return list of findings for rendered HTML text."""
    s = Scanner()
    s.feed(html_text)
    s.close()
    # Raw scan for sentinels outside parser callbacks (comments, decls).
    for m in re.finditer("\x00", html_text):
        line = html_text.count("\n", 0, m.start()) + 1
        col = m.start() - (html_text.rfind("\n", 0, m.start()) + 1)
        s.findings.append({
            "type": "sentinel-leak",
            "line": line,
            "col": col,
            "snippet": html_text[max(0, m.start() - 40):m.start() + 40],
            "detail": "\\x00 byte present in raw output",
        })
    # De-duplicate (type, line, col, snippet-prefix) pairs.
    seen, out = set(), []
    for f in s.findings:
        k = (f["type"], f["line"], f["col"], f["snippet"][:40])
        if k not in seen:
            seen.add(k)
            out.append(f)
    return out


SELFTEST = [
    # (name, html, expected finding types — empty means clean)
    ("clean link label url",
     '<a href="https://a.example" rel="nofollow">see https://b.example</a>',
     []),
    ("nested anchor in label",
     '<a href="https://a.example">see <a href="https://b.example">https://b.example</a></a>',
     ["nested-anchor"]),
    ("em inside alt",
     '<img src="https://x.example/i.png" alt="<em>star</em>" />',
     ["attr-markup"]),
    ("anchor inside alt",
     '<img src="u" alt="see <a href=\\"https://b.example\\">x</a>" />',
     ["attr-markup"]),
    ("em inside bare-url href",
     '<a href="https://x/<em>y</em>">https://x/<em>y</em></a>',
     ["attr-markup"]),
    ("sentinel swallowed into href",
     '<a href="https://a.example/\x00code\x00">x</a>',
     ["sentinel-leak"]),
    ("sentinel in text",
     "<p>hello \x00 world</p>",
     ["sentinel-leak"]),
    ("backtick in href",
     '<a href="https://a.example/`code`">x</a>',
     ["attr-backtick"]),
    ("clean emphasis in label",
     '<a href="https://x.example">a <em>b</em> and <strong>c</strong></a>',
     []),
    ("clean code after link",
     '<a href="https://a.example/" rel="nofollow">https://a.example/</a><code>code</code>',
     []),
    ("clean img plain alt",
     '<img src="https://x.example/i.png" alt="star and bold" loading="lazy" />',
     []),
    ("nested anchor paragraph",
     '<p>Read <a href="https://a.example">see <a href="https://b.example">b</a></a> today.</p>',
     ["nested-anchor"]),
]


def selftest():
    ok = True
    for name, html_text, expected in SELFTEST:
        got = {f["type"] for f in scan(html_text)}
        want = set(expected)
        if got != want:
            ok = False
            print("FAIL %-32s want=%s got=%s" % (name, want, got))
        else:
            print("pass %-32s %s" % (name, sorted(got) or ["clean"]))
    print("%d/%d vectors pass" % (
        sum(1 for n, h, e in SELFTEST
            if {f["type"] for f in scan(h)} == set(e)),
        len(SELFTEST)))
    return ok


def main(argv):
    if "--selftest" in argv:
        return 0 if selftest() else 1
    texts = []
    files = [a for a in argv[1:] if not a.startswith("--")]
    if files:
        for path in files:
            with open(path, "r", encoding="utf-8", errors="replace") as fh:
                texts.append((path, fh.read()))
    elif not sys.stdin.isatty():
        texts.append(("<stdin>", sys.stdin.read()))
    else:
        print(__doc__)
        return 2
    all_findings = []
    for name, text in texts:
        for f in scan(text):
            f["file"] = name
            all_findings.append(f)
    print(json.dumps({"ok": not all_findings,
                      "findings": all_findings}, indent=2))
    return 1 if all_findings else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))
