"""html_utils.py — shared HTML text-extraction helpers.

Consolidates the eleven independent ``strip_html`` copies that existed
across the plugin (QB2 dedup).  Two behaviours are needed:

strip_html(html, *, collapse_ws=True)
    • Strips HTML tags with a simple regex.
    • Decodes all HTML entities via ``html.unescape`` (handles &amp;, &nbsp;,
      &lt;, &gt;, &#NNN;, named entities like &copy;).
    • Collapses consecutive whitespace to a single space and strips leading/
      trailing whitespace (when *collapse_ws* is True, the default).
    • Handles None / empty input safely (returns "").
    • Used by: alignment_validator, alignment_skill, alignment_graph_preview,
      audit_mlo_drift, audit_rubrics, audit_conversion_readiness,
      extract_course_layout.

strip_html_preserve_ws(html)
    • Uses Python's stdlib ``HTMLParser`` so entity decoding is done by the
      parser automatically (HTMLParser.handle_data receives already-decoded
      text).  Whitespace inside text nodes is preserved as-is.
    • Falls back to a plain regex strip on malformed input.
    • Used by: preflight_checks, deterministic_checks.

Deliberately NOT consolidated here
    • ``canvas_api._strip_html_fragment`` — different name; one-line inline
      helper used only within canvas_api; intentionally left in place.
    • ``course_content_cache._strip_html`` — genuinely different behaviour:
      pre-strips ``<script>`` / ``<style>`` block content (not just the tags)
      and uses partial manual entity decode; left in place to preserve
      call-site semantics.
"""

from __future__ import annotations

import html as _html_module
import re
from html.parser import HTMLParser
from typing import Any


def strip_html(value: Any, *, collapse_ws: bool = True) -> str:
    """Strip HTML tags and decode entities.

    Args:
        value: Input string (or any value; coerced via ``str(value or "")``).
        collapse_ws: When True (default), collapse consecutive whitespace to a
            single space and strip leading/trailing whitespace.  Pass
            ``collapse_ws=False`` only when the caller needs to preserve
            indentation or newlines that are semantically significant.

    Returns:
        Plain-text string.
    """
    text = re.sub(r"<[^>]+>", " ", str(value or ""))
    text = _html_module.unescape(text)
    if collapse_ws:
        text = " ".join(text.split())
    return text


class _TextExtractor(HTMLParser):
    """Minimal HTMLParser subclass that accumulates text nodes."""

    def __init__(self) -> None:
        super().__init__()
        self._parts: list[str] = []

    def handle_data(self, data: str) -> None:
        self._parts.append(data)

    def get_text(self) -> str:
        return " ".join(self._parts)


def strip_html_preserve_ws(html: str | None) -> str:
    """Strip HTML tags while preserving whitespace inside text nodes.

    Uses ``HTMLParser`` so entity decoding is handled by the parser (no
    separate ``html.unescape`` call needed).  Whitespace is *not* collapsed —
    the raw text-node content is returned joined with a single space between
    nodes (matching the previous ``_TextExtractor``-based implementations in
    ``preflight_checks`` and ``deterministic_checks``).

    Falls back to a plain regex strip if the parser raises.
    """
    if not html:
        return ""
    parser = _TextExtractor()
    try:
        parser.feed(html)
        return parser.get_text()
    except (ValueError, AssertionError):
        return re.sub(r"<[^>]+>", " ", html)
