"""
HTML parser — uses trafilatura for boilerplate removal + lxml for structural parsing.

Strategy:
1. Run trafilatura to extract main content (removes nav, ads, footers)
2. Parse extracted HTML with lxml to identify headings, paragraphs, tables, code
3. Fall back to lxml full parse if trafilatura returns None
"""

from __future__ import annotations

import re
from collections.abc import Iterator

from .base import BaseParser, FileRecord, RawElement

# Admonition detection
_ADMONITION_CLASSES = re.compile(
    r"\b(note|warning|tip|caution|important|danger|alert|callout)\b", re.I
)


def _table_from_element(el) -> RawElement:  # type: ignore[no-untyped-def]
    """Convert an lxml table element to dual-format RawElement."""

    headers: list[str] = []
    rows: list[list[str]] = []
    has_header = False

    # Try thead for headers
    thead = el.find(".//thead")
    if thead is not None:
        for tr in thead.findall(".//tr"):
            headers = [
                (th.text_content() if hasattr(th, "text_content") else "").strip()
                for th in tr.findall(".//{http://www.w3.org/1999/xhtml}th") or tr.findall(".//th")
            ]
            if not headers:
                headers = [
                    (td.text_content() if hasattr(td, "text_content") else "").strip()
                    for td in tr.findall(".//{http://www.w3.org/1999/xhtml}td")
                    or tr.findall(".//td")
                ]
            has_header = bool(headers)
            break

    tbody_el = el.find(".//tbody")
    tbody = tbody_el if tbody_el is not None else el
    for tr in tbody.findall(".//tr"):
        cells = [
            (td.text_content() if hasattr(td, "text_content") else "").strip()
            for td in tr.findall(".//{http://www.w3.org/1999/xhtml}td") or tr.findall(".//td")
        ]
        if cells:
            rows.append(cells)

    # Build markdown
    if headers:
        sep = "|" + "|".join("---" for _ in headers) + "|"
        md_lines = ["|" + "|".join(headers) + "|", sep] + [
            "|" + "|".join(row) + "|" for row in rows
        ]
    else:
        md_lines = ["|" + "|".join(row) + "|" for row in rows]

    markdown = "\n".join(md_lines)

    return RawElement(
        element_type="table:simple",
        text=markdown,
        table_data={
            "markdown": markdown,
            "structured": {"headers": headers, "rows": rows},
            "has_header_row": has_header,
            "row_count": len(rows),
            "header_row_count": 1 if has_header else 0,
            "col_count": len(headers) if headers else (len(rows[0]) if rows else 0),
        },
        extra={"extraction_method": "html:lxml-table", "confidence": 0.88},
    )


def _walk_html(root) -> Iterator[RawElement]:  # type: ignore[no-untyped-def]
    """Walk lxml element tree and yield RawElement records."""

    HEADING_TAGS = {"h1", "h2", "h3", "h4", "h5", "h6"}
    SKIP_TAGS = {"script", "style", "nav", "header", "footer", "aside"}

    def text_content(el) -> str:  # type: ignore[no-untyped-def]
        return (el.text_content() if hasattr(el, "text_content") else "").strip()

    def tag_name(el) -> str:  # type: ignore[no-untyped-def]
        tag = el.tag
        if isinstance(tag, str):
            return tag.lower().split("}")[-1]
        return ""

    consumed: set[int] = set()  # ids of elements whose subtree is already yielded

    for el in root.iter():
        tag = tag_name(el)
        if not tag:
            continue

        if id(el) in consumed:
            continue

        if tag in SKIP_TAGS:
            # Mark all descendants as consumed too
            for desc in el.iter():
                consumed.add(id(desc))
            continue

        if tag in HEADING_TAGS:
            text = text_content(el)
            if text:
                level = int(tag[1])
                yield RawElement(
                    element_type="structural:section_header" if level > 1 else "structural:title",
                    text=text,
                    heading_level=min(level, 4),
                    detection_tier=1,
                    extra={"extraction_method": "html:semantic", "confidence": 0.97},
                )

        elif tag in ("pre", "code") and tag == "pre":
            # Only yield for <pre> (block code), not inline <code>
            text = text_content(el)
            if text:
                lang = el.get("class", "")
                m = re.search(r"language-(\w+)", lang)
                detected_lang = m.group(1) if m else None
                yield RawElement(
                    element_type="code:block",
                    text=text,
                    extra={
                        "extraction_method": "html:semantic",
                        "confidence": 0.98,
                        "language": detected_lang,
                    },
                )

        elif tag == "table":
            yield _table_from_element(el)

        elif tag in ("p", "li"):
            text = text_content(el)
            if not text:
                continue

            # Check for admonition patterns
            class_attr = el.get("class", "") or ""
            role_attr = el.get("role", "") or ""
            admonition_match = _ADMONITION_CLASSES.search(class_attr + " " + role_attr)
            if (
                admonition_match
                or tag == "li"
                and el.getparent() is not None
                and (el.getparent().get("class") or "")
                and _ADMONITION_CLASSES.search(el.getparent().get("class", ""))
            ):
                kind = admonition_match.group(1).lower() if admonition_match else "note"
                yield RawElement(
                    element_type="text:admonition",
                    text=text,
                    admonition_data={"kind": kind, "title": None},
                    extra={"extraction_method": "html:semantic", "confidence": 0.85},
                )
            elif tag == "li":
                yield RawElement(
                    element_type="list:item",
                    text=text,
                    extra={"extraction_method": "html:semantic", "confidence": 0.90},
                )
            else:
                yield RawElement(
                    element_type="text:narrative",
                    text=text,
                    extra={"extraction_method": "html:semantic", "confidence": 0.90},
                )

        elif tag == "blockquote":
            # Mark all descendants as consumed so inner <p> elements aren't re-emitted
            for desc in el.iter():
                consumed.add(id(desc))
            text = text_content(el)
            if text:
                # GitHub Flavored Markdown alerts in HTML
                first_line = text.split("\n")[0].strip().lower()
                gfm_kinds = {"note", "warning", "tip", "caution", "important"}
                if first_line.startswith("[!") and any(k in first_line for k in gfm_kinds):
                    kind = next((k for k in gfm_kinds if k in first_line), "note")
                    yield RawElement(
                        element_type="text:admonition",
                        text=text,
                        admonition_data={"kind": kind, "title": kind.title()},
                        extra={"extraction_method": "html:gfm-alert", "confidence": 0.95},
                    )
                else:
                    yield RawElement(
                        element_type="text:pull_quote",
                        text=text,
                        extra={"extraction_method": "html:semantic", "confidence": 0.85},
                    )


class HTMLParser(BaseParser):
    handles = [("text/html", None)]

    def parse(self, file_record: FileRecord) -> Iterator[RawElement]:
        try:
            import trafilatura  # type: ignore[import-untyped]
            from lxml import html as lxml_html
        except ImportError as e:
            raise ImportError("trafilatura and lxml required: pip install trafilatura lxml") from e

        raw_html = file_record.path.read_bytes()

        # Use trafilatura to find the main content tree (returns lxml element)
        # This removes nav, ads, footers while preserving HTML structure
        main_tree = trafilatura.utils.load_html(raw_html)

        if main_tree is not None:
            try:
                # trafilatura.extract_metadata can help find main <article>/<main> element
                # but the simplest approach is just parse the full lxml tree after
                # removing known boilerplate tags
                root = main_tree
                BOILERPLATE = {
                    "nav",
                    "header",
                    "footer",
                    "script",
                    "style",
                    "noscript",
                    "iframe",
                    "aside",
                }
                for tag in BOILERPLATE:
                    for el in root.findall(f".//{tag}"):
                        parent = el.getparent()
                        if parent is not None:
                            parent.remove(el)
                yield from _walk_html(root)
                return
            except Exception:
                pass

        # Final fallback: parse raw HTML
        try:
            root = lxml_html.fromstring(raw_html)
            yield from _walk_html(root)
        except Exception as e:
            raise RuntimeError(f"HTML parsing failed: {e}") from e
