# SPDX-FileCopyrightText: 2026 Logan Connolly
#
# SPDX-License-Identifier: AGPL-3.0-or-later

import argparse
import re
import sys
from collections import deque
from html.parser import HTMLParser
from pathlib import Path
from typing import override

__version__ = "1.0.0"


def main() -> None:
    cli = get_cli_parser()
    args = cli.parse_args()

    if args.file:
        fpath = Path(args.file)
        assert fpath.exists(), f"File does not exist: {args.file!r}"
        assert fpath.suffix == ".html", "Haitchify only processes HTML files."
        html_content = fpath.read_text()
    else:
        html_content = str(sys.stdin.read())

    parser = HaitchParser()
    parser.feed(html_content)
    haitch_code = parser.export()

    if args.prepend:
        print("import haitch as H", end="\n\n")

    print(haitch_code)


def get_cli_parser() -> argparse.ArgumentParser:
    cli = argparse.ArgumentParser(
        prog="haitchify",
        description="Convert HTML to compliant haitch code.",
        epilog="When a file is not provided, content is read from STDIN.",
    )
    cli.add_argument(
        "file",
        nargs="?",
        help="Optional file argument.",
    )
    cli.add_argument(
        "-p",
        "--prepend",
        action="store_true",
        help="Prepend haitch import to output.",
    )
    return cli


class HaitchParser(HTMLParser):
    def __init__(self) -> None:
        super().__init__()
        self._dom: str = ""
        self._number_of_root_elements: int = 0
        self._tag_stack: deque[str] = deque()

    def export(self) -> str:
        dom = self._dom.rstrip(",")
        is_fragment = self._number_of_root_elements > 1
        return f"H.fragment({dom})" if is_fragment else dom

    @override
    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        self._tag_stack.append(tag)

        # If tag is embedded in content, separate with comma.
        if self._dom and not self._dom.endswith((",", "(")):
            self._dom += ","

        attrs_ = handle_attributes(attrs)

        if tag in VOID_ELEMENTS:
            self._dom += f"H.{tag}({attrs_}),"
        else:
            tag_ = f"{tag}_" if tag in TAG_NAMES_TO_ESCAPE else tag
            self._dom += f"H.{tag_}({attrs_})(" if attrs_ else f"H.{tag_}("

    @override
    def handle_data(self, data: str) -> None:
        if sanitized := sanitize(data):
            self._dom += f'"{sanitized}"'

    @override
    def handle_endtag(self, tag: str) -> None:
        self._tag_stack.pop()

        if not self._tag_stack:
            self._number_of_root_elements += 1

        if tag not in VOID_ELEMENTS:
            # If content empty, drop superfluous parenthesis, otherwise close it.
            if self._dom.endswith("("):
                self._dom = self._dom[:-1] + ","
            else:
                self._dom += "),"


def handle_attributes(attrs: list[tuple[str, str | None]]) -> str:
    supported_attrs = {}
    extra_attrs = {}

    for key, value in attrs:
        match key:
            case key if key in ATTRIBUTES_TO_ESCAPE:
                supported_attrs[f"{key}_"] = value
            case key if key in SUPPORTED_ATTRIBUTES and "-" in key:
                supported_attrs[f"{key.replace('-', '_')}"] = value
            case key if key in SUPPORTED_ATTRIBUTES:
                supported_attrs[key] = value
            case _:
                extra_attrs[key] = value or True

    if extra_attrs:
        supported_attrs["extra_attrs"] = str(extra_attrs)

    return ", ".join(format_attribute(k, v) for k, v in supported_attrs.items())


def format_attribute(key: str, value: str | None) -> str:
    match key, value:
        case key, None:
            return f"{key}=True"
        case "extra_attrs", str():
            return f"{key}={value}"
        case key, str() if is_numeric(value):
            return f"{key}={value}"
        case _:
            return f'{key}="{sanitize(value)}"'


def is_numeric(value: str) -> bool:
    try:
        float(value)
    except ValueError:
        return False
    else:
        return True


def sanitize(value: str) -> str:
    if not value:
        return ""

    whitespace_char_table = {ord(char): None for char in "\t\n\r\v\f"}
    translated = value.translate(whitespace_char_table).strip()
    return re.sub(r"\s+", " ", translated).replace('"', '\\"')


VOID_ELEMENTS = {
    "area",
    "base",
    "br",
    "col",
    "embed",
    "hr",
    "img",
    "input",
    "line",
    "link",
    "meta",
    "source",
    "track",
    "wbr",
}

TAG_NAMES_TO_ESCAPE = {
    "and",
    "as",
    "assert",
    "break",
    "class",
    "continue",
    "def",
    "del",
    "elif",
    "else",
    "except",
    "finally",
    "for",
    "from",
    "global",
    "if",
    "import",
    "in",
    "is",
    "lambda",
    "nonlocal",
    "not",
    "or",
    "pass",
    "raise",
    "return",
    "try",
    "while",
    "with",
    "yield",
}

ATTRIBUTES_TO_ESCAPE = {
    "as",
    "async",
    "class",
    "for",
    "id",
    "is",
    "list",
    "type",
}

SUPPORTED_ATTRIBUTES = {
    "abbr",
    "accept",
    "accept-charset",
    "accesskey",
    "action",
    "alt",
    "autocapitalize",
    "autocomplete",
    "autocorrect",
    "autofocus",
    "autoplay",
    "capture",
    "charset",
    "checked",
    "cite",
    "cols",
    "colspan",
    "command",
    "commandfor",
    "content",
    "control",
    "controls",
    "controlslist",
    "coord",
    "crossorigin",
    "data",
    "datetime",
    "decoding",
    "default",
    "defer",
    "dir",
    "dirname",
    "disabled",
    "disablepictureinpicture",
    "disableremoteplayback",
    "download",
    "draggable",
    "elementtiming",
    "enctype",
    "enterkeyhint",
    "exportparts",
    "fetchpriority",
    "form",
    "formaction",
    "formenctype",
    "formmethod",
    "formnovalidate",
    "formtarget",
    "headers",
    "height",
    "hidden",
    "high",
    "href",
    "hreflang",
    "http-equiv",
    "inert",
    "inputmode",
    "integrity",
    "ismap",
    "itemid",
    "itemprop",
    "itemref",
    "itemscope",
    "itemtype",
    "kind",
    "label",
    "lang",
    "loading",
    "loop",
    "low",
    "max",
    "maxlength",
    "media",
    "method",
    "min",
    "minlength",
    "multiple",
    "muted",
    "name",
    "nomodule",
    "nonce",
    "novalidate",
    "onafterprint",
    "onbeforeprint",
    "onbeforeunload",
    "onblur",
    "onerror",
    "onfocus",
    "onhashchange",
    "onlanguagechange",
    "onload",
    "onmessage",
    "onoffline",
    "ononline",
    "onpopstate",
    "onredo",
    "onresize",
    "onstorage",
    "onundo",
    "onunload",
    "open",
    "optimum",
    "part",
    "pattern",
    "ping",
    "placeholder",
    "playsinline",
    "popover",
    "popovertarget",
    "popovertargetaction",
    "poster",
    "preload",
    "readonly",
    "referrerpolicy",
    "rel",
    "required",
    "reversed",
    "role",
    "rows",
    "rowspan",
    "scope",
    "selected",
    "shadowrootclonable",
    "shadowrootdelegatefocus",
    "shadowrootmode",
    "shape",
    "size",
    "sizes",
    "slot",
    "span",
    "spellcheck",
    "src",
    "srclang",
    "srcset",
    "start",
    "step",
    "style",
    "tabindex",
    "target",
    "title",
    "translate",
    "usemap",
    "value",
    "virtualkeyboardpolicy",
    "width",
    "wrap",
}
