"""
HSX-style HTML renderer.

Walks an s-expression tree and emits an HTML string.  HTML elements are
recognised by tag name; everything else is evaluated via the s-expression
evaluator and then rendered recursively.

Usage::

    from shared.sx import parse, make_env
    from shared.sx.html import render

    expr = parse('(div :class "card" (h1 "Hello") (p "World"))')
    html = render(expr)
    # → '<div class="card"><h1>Hello</h1><p>World</p></div>'

Components defined with ``defcomp`` are evaluated and their result is
rendered as HTML::

    env = {}
    evaluate(parse('(defcomp ~card (&key title &rest children) ...)'), env)
    html = render(parse('(~card :title "Hi" (p "body"))'), env)
"""

from __future__ import annotations

import contextvars
from typing import Any

from .types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol
from .evaluator import _eval as _raw_eval, _call_component as _raw_call_component, _expand_macro, _trampoline

def _eval(expr, env):
    """Evaluate and unwrap thunks — all html.py _eval calls are non-tail."""
    return _trampoline(_raw_eval(expr, env))

def _call_component(comp, raw_args, env):
    """Call component and unwrap thunks — non-tail in html.py."""
    return _trampoline(_raw_call_component(comp, raw_args, env))

# ContextVar for collecting CSS class names during render.
# Set to a set[str] to collect; None to skip.
css_class_collector: contextvars.ContextVar[set[str] | None] = contextvars.ContextVar(
    "css_class_collector", default=None
)

# ContextVar for SVG/MathML namespace auto-detection.
# When True, unknown tag names inside (svg ...) or (math ...) are treated as elements.
_svg_context: contextvars.ContextVar[bool] = contextvars.ContextVar(
    "_svg_context", default=False
)


class _RawHTML:
    """Marker for pre-rendered HTML that should not be escaped."""
    __slots__ = ("html",)

    def __init__(self, html: str):
        self.html = html


# ---------------------------------------------------------------------------
# HTML constants
# ---------------------------------------------------------------------------

# Tags that must not have a closing tag
VOID_ELEMENTS = frozenset({
    "area", "base", "br", "col", "embed", "hr", "img", "input",
    "link", "meta", "param", "source", "track", "wbr",
})

# Standard HTML tags (subset — any symbol that isn't recognised here will be
# treated as a function call and evaluated instead of rendered as a tag).
HTML_TAGS = frozenset({
    # Root / document
    "html", "head", "body",
    # Metadata
    "title", "meta", "link", "style", "script", "base", "noscript",
    # Sections
    "header", "footer", "main", "nav", "aside", "section", "article",
    "address", "hgroup",
    # Headings
    "h1", "h2", "h3", "h4", "h5", "h6",
    # Grouping
    "div", "p", "blockquote", "pre", "figure", "figcaption",
    "ul", "ol", "li", "dl", "dt", "dd", "hr",
    # Text
    "a", "span", "em", "strong", "small", "s", "cite", "q",
    "abbr", "code", "var", "samp", "kbd", "sub", "sup",
    "i", "b", "u", "mark", "ruby", "rt", "rp",
    "bdi", "bdo", "br", "wbr", "time", "data",
    # Edits
    "ins", "del",
    # Embedded
    "img", "picture", "source", "iframe", "embed", "object", "param",
    "video", "audio", "track", "canvas", "map", "area",
    "svg", "math",
    # SVG child elements
    "path", "circle", "ellipse", "line", "polygon", "polyline", "rect",
    "g", "defs", "use", "text", "tspan", "clipPath", "mask",
    "linearGradient", "radialGradient", "stop", "filter",
    "feGaussianBlur", "feOffset", "feMerge", "feMergeNode",
    "feTurbulence", "feColorMatrix", "feBlend",
    "feComponentTransfer", "feFuncR", "feFuncG", "feFuncB", "feFuncA",
    "feDisplacementMap", "feComposite", "feFlood", "feImage",
    "feMorphology", "feSpecularLighting", "feDiffuseLighting",
    "fePointLight", "feSpotLight", "feDistantLight",
    "animate", "animateTransform",
    # Table
    "table", "thead", "tbody", "tfoot", "tr", "th", "td",
    "caption", "colgroup", "col",
    # Forms
    "form", "fieldset", "legend", "label", "input", "button",
    "select", "option", "optgroup", "textarea", "output",
    "datalist", "progress", "meter",
    # Interactive
    "details", "summary", "dialog",
    # Template
    "template", "slot",
})

# Attributes that are boolean (presence = true, absence = false)
BOOLEAN_ATTRS = frozenset({
    "async", "autofocus", "autoplay", "checked", "controls",
    "default", "defer", "disabled", "formnovalidate", "hidden",
    "inert", "ismap", "loop", "multiple", "muted", "nomodule",
    "novalidate", "open", "playsinline", "readonly", "required",
    "reversed", "selected",
})


# ---------------------------------------------------------------------------
# Escaping
# ---------------------------------------------------------------------------

def escape_text(s: str) -> str:
    """Escape text content for safe HTML embedding."""
    return (
        s.replace("&", "&amp;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
    )


def escape_attr(s: str) -> str:
    """Escape an attribute value for safe embedding in double quotes."""
    return (
        s.replace("&", "&amp;")
        .replace('"', "&quot;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
    )


# ---------------------------------------------------------------------------
# Renderer
# ---------------------------------------------------------------------------

def render(expr: Any, env: dict[str, Any] | None = None) -> str:
    """Render an s-expression as an HTML string.

    *expr* can be:
    - A parsed (unevaluated) s-expression from ``parse()``
    - An already-evaluated value (string, list, etc.)

    *env* provides variable bindings for evaluation.
    """
    if env is None:
        env = {}
    return _render(expr, env)


def _render(expr: Any, env: dict[str, Any]) -> str:
    # --- nil / None / False → empty string --------------------------------
    if expr is None or expr is NIL or expr is False:
        return ""

    # --- True → empty (typically from a boolean expression, not content) ---
    if expr is True:
        return ""

    # --- pre-rendered HTML → pass through ----------------------------------
    if isinstance(expr, _RawHTML):
        return expr.html

    # --- string → escaped text --------------------------------------------
    if isinstance(expr, str):
        return escape_text(expr)

    # --- number → string --------------------------------------------------
    if isinstance(expr, (int, float)):
        return escape_text(str(expr))

    # --- symbol → evaluate then render ------------------------------------
    if isinstance(expr, Symbol):
        val = _eval(expr, env)
        return _render(val, env)

    # --- keyword → its name (unlikely in render context, but safe) --------
    if isinstance(expr, Keyword):
        return escape_text(expr.name)

    # --- list → main dispatch ---------------------------------------------
    if isinstance(expr, list):
        if not expr:
            return ""
        return _render_list(expr, env)

    # --- dict → skip (data, not renderable as HTML content) -----------------
    if isinstance(expr, dict):
        return ""

    # --- fallback ---------------------------------------------------------
    return escape_text(str(expr))


# ---------------------------------------------------------------------------
# Render-aware special forms
# ---------------------------------------------------------------------------
# These mirror the evaluator's special forms but call _render on the result
# branches, so that HTML tags inside (if ...), (when ...), (let ...) etc.
# are rendered correctly instead of being evaluated as function calls.

def _rsf_if(expr: list, env: dict[str, Any]) -> str:
    cond = _eval(expr[1], env)
    if cond and cond is not NIL:
        return _render(expr[2], env)
    if len(expr) > 3:
        return _render(expr[3], env)
    return ""


def _rsf_when(expr: list, env: dict[str, Any]) -> str:
    cond = _eval(expr[1], env)
    if cond and cond is not NIL:
        parts = []
        for body_expr in expr[2:]:
            parts.append(_render(body_expr, env))
        return "".join(parts)
    return ""


def _rsf_cond(expr: list, env: dict[str, Any]) -> str:
    from .types import Keyword as Kw
    clauses = expr[1:]
    if not clauses:
        return ""
    # Scheme-style: ((test body) ...)
    if isinstance(clauses[0], list) and len(clauses[0]) == 2:
        for clause in clauses:
            test = clause[0]
            if isinstance(test, Symbol) and test.name in ("else", ":else"):
                return _render(clause[1], env)
            if isinstance(test, Kw) and test.name == "else":
                return _render(clause[1], env)
            if _eval(test, env):
                return _render(clause[1], env)
    else:
        # Clojure-style: test body test body ...
        i = 0
        while i < len(clauses) - 1:
            test = clauses[i]
            result = clauses[i + 1]
            if isinstance(test, Kw) and test.name == "else":
                return _render(result, env)
            if isinstance(test, Symbol) and test.name in (":else", "else"):
                return _render(result, env)
            if _eval(test, env):
                return _render(result, env)
            i += 2
    return ""


def _rsf_let(expr: list, env: dict[str, Any]) -> str:
    bindings = expr[1]
    local = dict(env)
    if isinstance(bindings, list):
        if bindings and isinstance(bindings[0], list):
            for binding in bindings:
                var = binding[0]
                vname = var.name if isinstance(var, Symbol) else var
                local[vname] = _eval(binding[1], local)
        elif len(bindings) % 2 == 0:
            for i in range(0, len(bindings), 2):
                var = bindings[i]
                vname = var.name if isinstance(var, Symbol) else var
                local[vname] = _eval(bindings[i + 1], local)
    parts = []
    for body_expr in expr[2:]:
        parts.append(_render(body_expr, local))
    return "".join(parts)


def _rsf_begin(expr: list, env: dict[str, Any]) -> str:
    parts = []
    for sub in expr[1:]:
        parts.append(_render(sub, env))
    return "".join(parts)


def _rsf_define(expr: list, env: dict[str, Any]) -> str:
    _eval(expr, env)  # side effect: define in env
    return ""


def _rsf_defcomp(expr: list, env: dict[str, Any]) -> str:
    _eval(expr, env)  # side effect: register component
    return ""


def _render_lambda_call(fn: Lambda, args: tuple, env: dict[str, Any]) -> str:
    """Call a lambda and render the result — the body may contain HTML tags."""
    local = dict(fn.closure)
    local.update(env)
    for p, v in zip(fn.params, args):
        local[p] = v
    return _render(fn.body, local)


def _rsf_map(expr: list, env: dict[str, Any]) -> str:
    fn = _eval(expr[1], env)
    coll = _eval(expr[2], env)
    parts = []
    for item in coll:
        if isinstance(fn, Lambda):
            parts.append(_render_lambda_call(fn, (item,), env))
        elif callable(fn):
            parts.append(_render(fn(item), env))
        else:
            parts.append(_render(item, env))
    return "".join(parts)


def _rsf_map_indexed(expr: list, env: dict[str, Any]) -> str:
    fn = _eval(expr[1], env)
    coll = _eval(expr[2], env)
    parts = []
    for i, item in enumerate(coll):
        if isinstance(fn, Lambda):
            parts.append(_render_lambda_call(fn, (i, item), env))
        elif callable(fn):
            parts.append(_render(fn(i, item), env))
        else:
            parts.append(_render(item, env))
    return "".join(parts)


def _rsf_filter(expr: list, env: dict[str, Any]) -> str:
    # filter returns a list — render each kept item
    result = _eval(expr, env)
    return _render(result, env)


def _rsf_for_each(expr: list, env: dict[str, Any]) -> str:
    fn = _eval(expr[1], env)
    coll = _eval(expr[2], env)
    parts = []
    for item in coll:
        if isinstance(fn, Lambda):
            parts.append(_render_lambda_call(fn, (item,), env))
        elif callable(fn):
            parts.append(_render(fn(item), env))
        else:
            parts.append(_render(item, env))
    return "".join(parts)


_RENDER_FORMS: dict[str, Any] = {
    "if": _rsf_if,
    "when": _rsf_when,
    "cond": _rsf_cond,
    "let": _rsf_let,
    "let*": _rsf_let,
    "begin": _rsf_begin,
    "do": _rsf_begin,
    "define": _rsf_define,
    "defcomp": _rsf_defcomp,
    "map": _rsf_map,
    "map-indexed": _rsf_map_indexed,
    "filter": _rsf_filter,
    "for-each": _rsf_for_each,
    "defmacro": _rsf_define,   # side-effect only, returns ""
    "defhandler": _rsf_define, # side-effect only, returns ""
}


def _render_component(comp: Component, args: list, env: dict[str, Any]) -> str:
    """Render-aware component call: sets up scope then renders the body."""
    kwargs: dict[str, Any] = {}
    children: list[Any] = []
    i = 0
    while i < len(args):
        arg = args[i]
        if isinstance(arg, Keyword) and i + 1 < len(args):
            kwargs[arg.name] = _eval(args[i + 1], env)
            i += 2
        else:
            children.append(arg)
            i += 1

    local = dict(comp.closure)
    local.update(env)
    for p in comp.params:
        if p in kwargs:
            local[p] = kwargs[p]
        else:
            local[p] = NIL
    if comp.has_children:
        # Render children to HTML and wrap as _RawHTML to prevent re-escaping
        local["children"] = _RawHTML("".join(_render(c, env) for c in children))
    return _render(comp.body, local)


def _render_list(expr: list, env: dict[str, Any]) -> str:
    """Render a list expression — could be an HTML element, special form,
    component call, or data list."""
    head = expr[0]

    if isinstance(head, Symbol):
        name = head.name

        # --- raw! → unescaped HTML ----------------------------------------
        if name == "raw!":
            parts = []
            for arg in expr[1:]:
                val = _eval(arg, env)
                if isinstance(val, _RawHTML):
                    parts.append(val.html)
                elif isinstance(val, str):
                    parts.append(val)
                elif val is not None and val is not NIL:
                    parts.append(str(val))
            return "".join(parts)

        # --- <> → fragment (render children, no wrapper) ------------------
        if name == "<>":
            return "".join(_render(child, env) for child in expr[1:])

        # --- html: prefix → force tag rendering --------------------------
        if name.startswith("html:"):
            return _render_element(name[5:], expr[1:], env)

        # --- Render-aware special forms --------------------------------------
        # Check BEFORE HTML_TAGS because some names overlap (e.g. `map`).
        # But if the name is ALSO an HTML tag and (a) first arg is a Keyword
        # or (b) we're inside SVG/MathML context, it's a tag call.
        rsf = _RENDER_FORMS.get(name)
        if rsf is not None:
            if name in HTML_TAGS and (
                (len(expr) > 1 and isinstance(expr[1], Keyword))
                or _svg_context.get(False)
            ):
                return _render_element(name, expr[1:], env)
            return rsf(expr, env)

        # --- Macro expansion → expand then render --------------------------
        if name in env:
            val = env[name]
            if isinstance(val, Macro):
                expanded = _expand_macro(val, expr[1:], env)
                return _render(expanded, env)

        # --- HTML tag → render as element ---------------------------------
        if name in HTML_TAGS:
            return _render_element(name, expr[1:], env)

        # --- Component (~prefix) → render-aware component call ------------
        if name.startswith("~"):
            val = env.get(name)
            if isinstance(val, Component):
                return _render_component(val, expr[1:], env)
            # Fall through to evaluation

        # --- Custom element (hyphenated name with keyword attrs) → tag ----
        if "-" in name and len(expr) > 1 and isinstance(expr[1], Keyword):
            return _render_element(name, expr[1:], env)

        # --- SVG/MathML context → unknown names are child elements --------
        if _svg_context.get(False):
            return _render_element(name, expr[1:], env)

        # --- Other special forms / function calls → evaluate then render ---
        result = _eval(expr, env)
        return _render(result, env)

    # --- head is lambda or other callable → evaluate then render ----------
    if isinstance(head, (Lambda, list)):
        result = _eval(expr, env)
        return _render(result, env)

    # --- data list → render each item -------------------------------------
    return "".join(_render(item, env) for item in expr)


def _render_element(tag: str, args: list, env: dict[str, Any]) -> str:
    """Render an HTML element: extract attrs (keywords), render children."""
    attrs: dict[str, Any] = {}
    children: list[Any] = []

    i = 0
    while i < len(args):
        arg = args[i]
        # Keyword followed by value → attribute
        if isinstance(arg, Keyword) and i + 1 < len(args):
            attr_name = arg.name
            attr_val = _eval(args[i + 1], env)
            attrs[attr_name] = attr_val
            i += 2
        else:
            children.append(arg)
            i += 1

    # Handle :style StyleValue — convert to class and register CSS rule
    style_val = attrs.get("style")
    if isinstance(style_val, StyleValue):
        from .css_registry import register_generated_rule
        register_generated_rule(style_val)
        # Merge into :class
        existing_class = attrs.get("class")
        if existing_class and existing_class is not NIL and existing_class is not False:
            attrs["class"] = f"{existing_class} {style_val.class_name}"
        else:
            attrs["class"] = style_val.class_name
        del attrs["style"]

    # Collect CSS classes if collector is active
    class_val = attrs.get("class")
    if class_val is not None and class_val is not NIL and class_val is not False:
        collector = css_class_collector.get(None)
        if collector is not None:
            collector.update(str(class_val).split())

    # Build opening tag
    parts = [f"<{tag}"]
    for attr_name, attr_val in attrs.items():
        if attr_val is None or attr_val is NIL or attr_val is False:
            continue
        if attr_name in BOOLEAN_ATTRS:
            if attr_val:
                parts.append(f" {attr_name}")
        elif attr_val is True:
            parts.append(f" {attr_name}")
        elif isinstance(attr_val, dict):
            from .parser import serialize as _sx_serialize
            parts.append(f' {attr_name}="{escape_attr(_sx_serialize(attr_val))}"')
        else:
            parts.append(f' {attr_name}="{escape_attr(str(attr_val))}"')
    parts.append(">")

    opening = "".join(parts)

    # Void elements: no closing tag, no children
    if tag in VOID_ELEMENTS:
        return opening

    # SVG/MathML namespace auto-detection: set context for children
    token = None
    if tag in ("svg", "math"):
        token = _svg_context.set(True)

    try:
        child_html = "".join(_render(child, env) for child in children)
    finally:
        if token is not None:
            _svg_context.reset(token)

    return f"{opening}{child_html}</{tag}>"
