#!/usr/bin/env python3
"""Faithful restyle of faculty-authored HTML sections.

When building mockups or derived content from faculty instruction text, the
text must survive verbatim — only structural tags get restyled. This module
extracts a named section, injects inline styles into structural tags while
preserving every text node and every <a href>/file/verifier token, and verifies
tag-stripped byte-equality before any "fidelity" claim is made.

Invocation (consistent with other scripts):
    ${ASUCB_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}}/bin/python3 \
        scripts/faithful_restyle.py --source FILE --section NAME --out FILE
"""

import argparse
import html as html_lib
import re
import sys

from bs4 import BeautifulSoup, NavigableString

# Default inline styles per structural tag. Text nodes and attributes are never
# touched — only the style attribute is set/merged.
DEFAULT_STYLE_MAP = {
    "ul": "margin: 0 0 16px 24px; padding: 0; list-style: disc;",
    "ol": "margin: 0 0 16px 24px; padding: 0; list-style: decimal;",
    "li": "margin: 0 0 8px 0; line-height: 1.6; color: #333;",
    "p": "margin: 0 0 14px 0; line-height: 1.6; color: #333;",
    "h3": "color: #8C1D40; font-size: 20px; margin: 24px 0 10px 0;",
    "h4": "color: #8C1D40; font-size: 17px; margin: 20px 0 8px 0;",
    "a": "color: #8C1D40; text-decoration: underline;",
    "pre": "background: #f5f5f5; padding: 12px; border-radius: 6px; overflow: auto;",
}

_HEADING_TAGS = ("h2", "h3", "h4")
_HEADING_LEVEL = {"h1": 1, "h2": 2, "h3": 3, "h4": 4, "h5": 5, "h6": 6}

# Tags whose entire subtree (tag + content) is active/executable and must
# never survive into restyled output. Neither carries faculty prose text —
# BeautifulSoup stores <script>/<style> content as Script/Stylesheet
# NavigableString subtypes, which get_text() already excludes by default —
# so dropping them does not affect verify_verbatim's text comparison.
_ACTIVE_TAGS = ("script", "style")

# Attributes whose values are URLs and must be checked for script-executing
# schemes before being allowed to survive restyle.
_URL_ATTRS = frozenset({"href", "src", "action", "formaction"})

# URL schemes that execute script/code when navigated to or loaded.
_DANGEROUS_URL_PREFIXES = ("javascript:", "vbscript:")


def _is_dangerous_url(value):
    """True if a URL attribute value uses a script-executing scheme.

    Normalizes HTML-entity escapes and strips whitespace/control characters
    first so simple evasion tricks (case, embedded tabs/newlines, leading
    whitespace) don't slip past a naive prefix check.
    """
    if not value:
        return False
    decoded = html_lib.unescape(value)
    normalized = "".join(ch for ch in decoded if ch not in "\t\n\r").strip().lower()
    return normalized.startswith(_DANGEROUS_URL_PREFIXES)


def _norm(text):
    """Strip tags' residue: html-unescape + collapse whitespace."""
    return re.sub(r"\s+", " ", html_lib.unescape(text)).strip()


def extract_section(source_html, heading_text):
    """Return inner HTML of the section under a heading whose text == heading_text.

    The section spans the content AFTER the matching <h2>-<h4> heading, up to the
    next heading of the same or higher level (lower numeric level). Returns "" if
    no matching heading is found.
    """
    soup = BeautifulSoup(source_html, "html.parser")
    target = None
    for tag in soup.find_all(_HEADING_TAGS):
        if _norm(tag.get_text()) == _norm(heading_text):
            target = tag
            break
    if target is None:
        return ""

    start_level = _HEADING_LEVEL.get(target.name, 6)
    parts = []
    for sib in target.next_siblings:
        name = getattr(sib, "name", None)
        if name in _HEADING_LEVEL and _HEADING_LEVEL[name] <= start_level:
            break
        parts.append(str(sib))
    return "".join(parts).strip()


def _merge_style(existing, addition):
    """Append addition to an existing style attribute without clobbering it."""
    existing = (existing or "").strip()
    if not existing:
        return addition
    if not existing.endswith(";"):
        existing += ";"
    return f"{existing} {addition}"


def restyle(fragment_html, style_map=None):
    """Inject inline styles into structural tags; preserve all text + hrefs/tokens.

    Only the `style` attribute of tags in style_map is set/merged. Text nodes,
    href values, and embedded tokens (e.g. verifier=ABC123) are left untouched.

    Active/unsafe content is stripped, not preserved: <script>/<style>
    subtrees are removed, event-handler (on*) attributes are dropped, and
    javascript:/vbscript: URL values are dropped. None of this carries
    visible faculty text, so it does not affect verify_verbatim — but if the
    restyled fragment is ever rendered inline (mockups, derived artifacts),
    leaving it in place would let it execute.
    """
    style_map = style_map or DEFAULT_STYLE_MAP
    soup = BeautifulSoup(fragment_html, "html.parser")

    for tag in soup.find_all(_ACTIVE_TAGS):
        tag.decompose()

    for tag in soup.find_all(True):
        for attr in list(tag.attrs):
            attr_lower = attr.lower()
            if attr_lower.startswith("on") or (
                attr_lower in _URL_ATTRS and _is_dangerous_url(tag.get(attr))
            ):
                del tag[attr]
        style = style_map.get(tag.name)
        if style:
            tag["style"] = _merge_style(tag.get("style"), style)
    return str(soup)


def verify_verbatim(source_fragment, output_fragment):
    """True iff the tag-stripped, normalized text of both fragments is equal."""
    src_text = BeautifulSoup(source_fragment, "html.parser").get_text()
    out_text = BeautifulSoup(output_fragment, "html.parser").get_text()
    return _norm(src_text) == _norm(out_text)


def main(argv=None):
    parser = argparse.ArgumentParser(
        description="Extract + restyle a faculty HTML section with verbatim verification."
    )
    parser.add_argument("--source", required=True, help="Source HTML file")
    parser.add_argument("--section", required=True, help="Heading text of the section to extract")
    parser.add_argument("--out", required=True, help="Output file for the restyled section")
    args = parser.parse_args(argv)

    with open(args.source, "r", encoding="utf-8") as f:
        source_html = f.read()

    fragment = extract_section(source_html, args.section)
    if not fragment:
        print(f"FAIL (drift): section '{args.section}' not found in source", file=sys.stderr)
        return 1

    restyled = restyle(fragment)

    with open(args.out, "w", encoding="utf-8") as f:
        f.write(restyled)

    if verify_verbatim(fragment, restyled):
        print("PASS")
        return 0
    print("FAIL (drift)", file=sys.stderr)
    return 1


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