"""
Async s-expression evaluator and HTML renderer.

Mirrors the sync evaluator (evaluator.py) and HTML renderer (html.py) but
every step is ``async`` so I/O primitives can be ``await``ed inline.

This is the execution engine for ``defhandler`` — handlers contain I/O
calls (``query``, ``service``, ``request-arg``, etc.) interleaved with
control flow (``if``, ``let``, ``map``, ``when``).  The sync
collect-then-substitute resolver can't handle data dependencies between
I/O results and control flow, so handlers need inline async evaluation.

Usage::

    from shared.sx.async_eval import async_render

    html = await async_render(handler_def.body, env, ctx)
"""

from __future__ import annotations

from typing import Any

from .types import Component, Keyword, Lambda, Macro, NIL, Symbol
from .evaluator import _expand_macro, EvalError
from .primitives import _PRIMITIVES
from .primitives_io import IO_PRIMITIVES, RequestContext, execute_io
from .parser import SxExpr, serialize
from .html import (
    HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS,
    escape_text, escape_attr, _RawHTML, css_class_collector,
)


# ---------------------------------------------------------------------------
# Async evaluate
# ---------------------------------------------------------------------------

async def async_eval(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any:
    """Evaluate *expr* in *env*, awaiting I/O primitives inline."""
    # --- literals ---
    if isinstance(expr, (int, float, str, bool)):
        return expr
    if expr is None or expr is NIL:
        return NIL

    # --- symbol lookup ---
    if isinstance(expr, Symbol):
        name = expr.name
        if name in env:
            return env[name]
        if name in _PRIMITIVES:
            return _PRIMITIVES[name]
        if name == "true":
            return True
        if name == "false":
            return False
        if name == "nil":
            return NIL
        raise EvalError(f"Undefined symbol: {name}")

    # --- keyword ---
    if isinstance(expr, Keyword):
        return expr.name

    # --- dict literal ---
    if isinstance(expr, dict):
        return {k: await async_eval(v, env, ctx) for k, v in expr.items()}

    # --- list ---
    if not isinstance(expr, list):
        return expr
    if not expr:
        return []

    head = expr[0]

    if not isinstance(head, (Symbol, Lambda, list)):
        return [await async_eval(x, env, ctx) for x in expr]

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

        # I/O primitives — await inline
        if name in IO_PRIMITIVES:
            args, kwargs = await _parse_io_args(expr[1:], env, ctx)
            return await execute_io(name, args, kwargs, ctx)

        # Special forms
        sf = _ASYNC_SPECIAL_FORMS.get(name)
        if sf is not None:
            return await sf(expr, env, ctx)

        ho = _ASYNC_HO_FORMS.get(name)
        if ho is not None:
            return await ho(expr, env, ctx)

        # Macro expansion
        if name in env:
            val = env[name]
            if isinstance(val, Macro):
                expanded = _expand_macro(val, expr[1:], env)
                return await async_eval(expanded, env, ctx)

        # Render forms in eval position — delegate to renderer and return
        # as _RawHTML so it won't be double-escaped when used in render
        # context later.  Allows (let ((x (<> ...))) ...) etc.
        if name in ("<>", "raw!") or name in HTML_TAGS:
            html = await _arender(expr, env, ctx)
            return _RawHTML(html)

    # --- function / lambda call ---
    fn = await async_eval(head, env, ctx)
    args = [await async_eval(a, env, ctx) for a in expr[1:]]

    if callable(fn) and not isinstance(fn, (Lambda, Component)):
        return fn(*args)
    if isinstance(fn, Lambda):
        return await _async_call_lambda(fn, args, env, ctx)
    if isinstance(fn, Component):
        return await _async_call_component(fn, expr[1:], env, ctx)
    raise EvalError(f"Not callable: {fn!r}")


async def _parse_io_args(
    exprs: list[Any], env: dict[str, Any], ctx: RequestContext,
) -> tuple[list[Any], dict[str, Any]]:
    """Parse and evaluate I/O node args."""
    args: list[Any] = []
    kwargs: dict[str, Any] = {}
    i = 0
    while i < len(exprs):
        item = exprs[i]
        if isinstance(item, Keyword) and i + 1 < len(exprs):
            kwargs[item.name] = await async_eval(exprs[i + 1], env, ctx)
            i += 2
        else:
            args.append(await async_eval(item, env, ctx))
            i += 1
    return args, kwargs


async def _async_call_lambda(
    fn: Lambda, args: list[Any], caller_env: dict[str, Any], ctx: RequestContext,
) -> Any:
    if len(args) != len(fn.params):
        raise EvalError(f"{fn!r} expects {len(fn.params)} args, got {len(args)}")
    local = dict(fn.closure)
    local.update(caller_env)
    for p, v in zip(fn.params, args):
        local[p] = v
    return await async_eval(fn.body, local, ctx)


async def _async_call_component(
    comp: Component, raw_args: list[Any], env: dict[str, Any], ctx: RequestContext,
) -> Any:
    kwargs: dict[str, Any] = {}
    children: list[Any] = []
    i = 0
    while i < len(raw_args):
        arg = raw_args[i]
        if isinstance(arg, Keyword) and i + 1 < len(raw_args):
            kwargs[arg.name] = await async_eval(raw_args[i + 1], env, ctx)
            i += 2
        else:
            children.append(await async_eval(arg, env, ctx))
            i += 1
    local = dict(comp.closure)
    local.update(env)
    for p in comp.params:
        local[p] = kwargs.get(p, NIL)
    if comp.has_children:
        local["children"] = children
    return await async_eval(comp.body, local, ctx)


# ---------------------------------------------------------------------------
# Async special forms
# ---------------------------------------------------------------------------

async def _asf_if(expr, env, ctx):
    cond = await async_eval(expr[1], env, ctx)
    if cond and cond is not NIL:
        return await async_eval(expr[2], env, ctx)
    if len(expr) > 3:
        return await async_eval(expr[3], env, ctx)
    return NIL


async def _asf_when(expr, env, ctx):
    cond = await async_eval(expr[1], env, ctx)
    if cond and cond is not NIL:
        result = NIL
        for body_expr in expr[2:]:
            result = await async_eval(body_expr, env, ctx)
        return result
    return NIL


async def _asf_and(expr, env, ctx):
    result: Any = True
    for arg in expr[1:]:
        result = await async_eval(arg, env, ctx)
        if not result:
            return result
    return result


async def _asf_or(expr, env, ctx):
    result: Any = False
    for arg in expr[1:]:
        result = await async_eval(arg, env, ctx)
        if result:
            return result
    return result


async def _asf_let(expr, env, ctx):
    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] = await async_eval(binding[1], local, ctx)
        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] = await async_eval(bindings[i + 1], local, ctx)
    result: Any = NIL
    for body_expr in expr[2:]:
        result = await async_eval(body_expr, local, ctx)
    return result


async def _asf_lambda(expr, env, ctx):
    params_expr = expr[1]
    param_names = []
    for p in params_expr:
        if isinstance(p, Symbol):
            param_names.append(p.name)
        elif isinstance(p, str):
            param_names.append(p)
    return Lambda(param_names, expr[2], dict(env))


async def _asf_define(expr, env, ctx):
    name_sym = expr[1]
    value = await async_eval(expr[2], env, ctx)
    if isinstance(value, Lambda) and value.name is None:
        value.name = name_sym.name
    env[name_sym.name] = value
    return value


async def _asf_defcomp(expr, env, ctx):
    from .evaluator import _sf_defcomp
    return _sf_defcomp(expr, env)


async def _asf_defmacro(expr, env, ctx):
    from .evaluator import _sf_defmacro
    return _sf_defmacro(expr, env)


async def _asf_defhandler(expr, env, ctx):
    from .evaluator import _sf_defhandler
    return _sf_defhandler(expr, env)


async def _asf_begin(expr, env, ctx):
    result: Any = NIL
    for sub in expr[1:]:
        result = await async_eval(sub, env, ctx)
    return result


async def _asf_quote(expr, env, ctx):
    return expr[1] if len(expr) > 1 else NIL


async def _asf_quasiquote(expr, env, ctx):
    return await _async_qq_expand(expr[1], env, ctx)


async def _async_qq_expand(template, env, ctx):
    if not isinstance(template, list):
        return template
    if not template:
        return []
    head = template[0]
    if isinstance(head, Symbol):
        if head.name == "unquote":
            return await async_eval(template[1], env, ctx)
        if head.name == "splice-unquote":
            raise EvalError("splice-unquote not inside a list")
    result: list[Any] = []
    for item in template:
        if (isinstance(item, list) and len(item) == 2
                and isinstance(item[0], Symbol) and item[0].name == "splice-unquote"):
            spliced = await async_eval(item[1], env, ctx)
            if isinstance(spliced, list):
                result.extend(spliced)
            elif spliced is not None and spliced is not NIL:
                result.append(spliced)
        else:
            result.append(await _async_qq_expand(item, env, ctx))
    return result


async def _asf_cond(expr, env, ctx):
    clauses = expr[1:]
    if not clauses:
        return NIL
    if (isinstance(clauses[0], list) and len(clauses[0]) == 2
            and not (isinstance(clauses[0][0], Symbol) and clauses[0][0].name in (
                "=", "<", ">", "<=", ">=", "!=", "and", "or"))):
        for clause in clauses:
            test = clause[0]
            if isinstance(test, Symbol) and test.name in ("else", ":else"):
                return await async_eval(clause[1], env, ctx)
            if isinstance(test, Keyword) and test.name == "else":
                return await async_eval(clause[1], env, ctx)
            if await async_eval(test, env, ctx):
                return await async_eval(clause[1], env, ctx)
    else:
        i = 0
        while i < len(clauses) - 1:
            test = clauses[i]
            result = clauses[i + 1]
            if isinstance(test, Keyword) and test.name == "else":
                return await async_eval(result, env, ctx)
            if isinstance(test, Symbol) and test.name in (":else", "else"):
                return await async_eval(result, env, ctx)
            if await async_eval(test, env, ctx):
                return await async_eval(result, env, ctx)
            i += 2
    return NIL


async def _asf_case(expr, env, ctx):
    match_val = await async_eval(expr[1], env, ctx)
    clauses = expr[2:]
    i = 0
    while i < len(clauses) - 1:
        test = clauses[i]
        result = clauses[i + 1]
        if isinstance(test, Keyword) and test.name == "else":
            return await async_eval(result, env, ctx)
        if isinstance(test, Symbol) and test.name in (":else", "else"):
            return await async_eval(result, env, ctx)
        if match_val == await async_eval(test, env, ctx):
            return await async_eval(result, env, ctx)
        i += 2
    return NIL


async def _asf_thread_first(expr, env, ctx):
    result = await async_eval(expr[1], env, ctx)
    for form in expr[2:]:
        if isinstance(form, list):
            fn = await async_eval(form[0], env, ctx)
            args = [result] + [await async_eval(a, env, ctx) for a in form[1:]]
        else:
            fn = await async_eval(form, env, ctx)
            args = [result]
        if callable(fn) and not isinstance(fn, (Lambda, Component)):
            result = fn(*args)
        elif isinstance(fn, Lambda):
            result = await _async_call_lambda(fn, args, env, ctx)
        else:
            raise EvalError(f"-> form not callable: {fn!r}")
    return result


async def _asf_set_bang(expr, env, ctx):
    value = await async_eval(expr[2], env, ctx)
    env[expr[1].name] = value
    return value


_ASYNC_SPECIAL_FORMS: dict[str, Any] = {
    "if": _asf_if,
    "when": _asf_when,
    "cond": _asf_cond,
    "case": _asf_case,
    "and": _asf_and,
    "or": _asf_or,
    "let": _asf_let,
    "let*": _asf_let,
    "lambda": _asf_lambda,
    "fn": _asf_lambda,
    "define": _asf_define,
    "defcomp": _asf_defcomp,
    "defmacro": _asf_defmacro,
    "defhandler": _asf_defhandler,
    "begin": _asf_begin,
    "do": _asf_begin,
    "quote": _asf_quote,
    "quasiquote": _asf_quasiquote,
    "->": _asf_thread_first,
    "set!": _asf_set_bang,
}


# ---------------------------------------------------------------------------
# Async higher-order forms
# ---------------------------------------------------------------------------

async def _aho_map(expr, env, ctx):
    fn = await async_eval(expr[1], env, ctx)
    coll = await async_eval(expr[2], env, ctx)
    results = []
    for item in coll:
        if isinstance(fn, Lambda):
            results.append(await _async_call_lambda(fn, [item], env, ctx))
        elif callable(fn):
            results.append(fn(item))
        else:
            raise EvalError(f"map requires callable, got {type(fn).__name__}")
    return results


async def _aho_map_indexed(expr, env, ctx):
    fn = await async_eval(expr[1], env, ctx)
    coll = await async_eval(expr[2], env, ctx)
    results = []
    for i, item in enumerate(coll):
        if isinstance(fn, Lambda):
            results.append(await _async_call_lambda(fn, [i, item], env, ctx))
        elif callable(fn):
            results.append(fn(i, item))
        else:
            raise EvalError(f"map-indexed requires callable, got {type(fn).__name__}")
    return results


async def _aho_filter(expr, env, ctx):
    fn = await async_eval(expr[1], env, ctx)
    coll = await async_eval(expr[2], env, ctx)
    results = []
    for item in coll:
        if isinstance(fn, Lambda):
            val = await _async_call_lambda(fn, [item], env, ctx)
        elif callable(fn):
            val = fn(item)
        else:
            raise EvalError(f"filter requires callable, got {type(fn).__name__}")
        if val:
            results.append(item)
    return results


async def _aho_reduce(expr, env, ctx):
    fn = await async_eval(expr[1], env, ctx)
    acc = await async_eval(expr[2], env, ctx)
    coll = await async_eval(expr[3], env, ctx)
    for item in coll:
        acc = await _async_call_lambda(fn, [acc, item], env, ctx) if isinstance(fn, Lambda) else fn(acc, item)
    return acc


async def _aho_some(expr, env, ctx):
    fn = await async_eval(expr[1], env, ctx)
    coll = await async_eval(expr[2], env, ctx)
    for item in coll:
        result = await _async_call_lambda(fn, [item], env, ctx) if isinstance(fn, Lambda) else fn(item)
        if result:
            return result
    return NIL


async def _aho_every(expr, env, ctx):
    fn = await async_eval(expr[1], env, ctx)
    coll = await async_eval(expr[2], env, ctx)
    for item in coll:
        if not (await _async_call_lambda(fn, [item], env, ctx) if isinstance(fn, Lambda) else fn(item)):
            return False
    return True


async def _aho_for_each(expr, env, ctx):
    fn = await async_eval(expr[1], env, ctx)
    coll = await async_eval(expr[2], env, ctx)
    for item in coll:
        if isinstance(fn, Lambda):
            await _async_call_lambda(fn, [item], env, ctx)
        elif callable(fn):
            fn(item)
    return NIL


_ASYNC_HO_FORMS: dict[str, Any] = {
    "map": _aho_map,
    "map-indexed": _aho_map_indexed,
    "filter": _aho_filter,
    "reduce": _aho_reduce,
    "some": _aho_some,
    "every?": _aho_every,
    "for-each": _aho_for_each,
}


# ---------------------------------------------------------------------------
# Async HTML renderer
# ---------------------------------------------------------------------------

async def async_render(
    expr: Any,
    env: dict[str, Any],
    ctx: RequestContext | None = None,
) -> str:
    """Render an s-expression to HTML, awaiting I/O primitives inline."""
    if ctx is None:
        ctx = RequestContext()
    return await _arender(expr, env, ctx)


async def _arender(expr: Any, env: dict[str, Any], ctx: RequestContext) -> str:
    if expr is None or expr is NIL or expr is False or expr is True:
        return ""
    if isinstance(expr, _RawHTML):
        return expr.html
    if isinstance(expr, str):
        return escape_text(expr)
    if isinstance(expr, (int, float)):
        return escape_text(str(expr))
    if isinstance(expr, Symbol):
        val = await async_eval(expr, env, ctx)
        return await _arender(val, env, ctx)
    if isinstance(expr, Keyword):
        return escape_text(expr.name)
    if isinstance(expr, list):
        if not expr:
            return ""
        return await _arender_list(expr, env, ctx)
    if isinstance(expr, dict):
        return ""
    return escape_text(str(expr))


async def _arender_list(expr: list, env: dict[str, Any], ctx: RequestContext) -> str:
    head = expr[0]

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

        # I/O primitive — await, then render result
        if name in IO_PRIMITIVES:
            result = await async_eval(expr, env, ctx)
            return await _arender(result, env, ctx)

        # raw!
        if name == "raw!":
            parts = []
            for arg in expr[1:]:
                val = await async_eval(arg, env, ctx)
                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)

        # <>
        if name == "<>":
            parts = []
            for child in expr[1:]:
                parts.append(await _arender(child, env, ctx))
            return "".join(parts)

        # Render-aware special forms
        arsf = _ASYNC_RENDER_FORMS.get(name)
        if arsf is not None:
            return await arsf(expr, env, ctx)

        # Macro expansion
        if name in env:
            val = env[name]
            if isinstance(val, Macro):
                expanded = _expand_macro(val, expr[1:], env)
                return await _arender(expanded, env, ctx)

        # HTML tag
        if name in HTML_TAGS:
            return await _arender_element(name, expr[1:], env, ctx)

        # Component
        if name.startswith("~"):
            val = env.get(name)
            if isinstance(val, Component):
                return await _arender_component(val, expr[1:], env, ctx)

        # Fallback — evaluate then render
        result = await async_eval(expr, env, ctx)
        return await _arender(result, env, ctx)

    if isinstance(head, (Lambda, list)):
        result = await async_eval(expr, env, ctx)
        return await _arender(result, env, ctx)

    # Data list
    parts = []
    for item in expr:
        parts.append(await _arender(item, env, ctx))
    return "".join(parts)


async def _arender_element(
    tag: str, args: list, env: dict[str, Any], ctx: RequestContext,
) -> str:
    attrs: dict[str, Any] = {}
    children: list[Any] = []
    i = 0
    while i < len(args):
        arg = args[i]
        if isinstance(arg, Keyword) and i + 1 < len(args):
            attr_val = await async_eval(args[i + 1], env, ctx)
            attrs[arg.name] = attr_val
            i += 2
        else:
            children.append(arg)
            i += 1

    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())

    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}")
        else:
            parts.append(f' {attr_name}="{escape_attr(str(attr_val))}"')
    parts.append(">")
    opening = "".join(parts)

    if tag in VOID_ELEMENTS:
        return opening

    child_parts = []
    for child in children:
        child_parts.append(await _arender(child, env, ctx))
    return f"{opening}{''.join(child_parts)}</{tag}>"


async def _arender_component(
    comp: Component, args: list, env: dict[str, Any], ctx: RequestContext,
) -> str:
    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] = await async_eval(args[i + 1], env, ctx)
            i += 2
        else:
            children.append(arg)
            i += 1
    local = dict(comp.closure)
    local.update(env)
    for p in comp.params:
        local[p] = kwargs.get(p, NIL)
    if comp.has_children:
        child_html = []
        for c in children:
            child_html.append(await _arender(c, env, ctx))
        local["children"] = _RawHTML("".join(child_html))
    return await _arender(comp.body, local, ctx)


async def _arender_lambda(
    fn: Lambda, args: tuple, env: dict[str, Any], ctx: RequestContext,
) -> str:
    local = dict(fn.closure)
    local.update(env)
    for p, v in zip(fn.params, args):
        local[p] = v
    return await _arender(fn.body, local, ctx)


# ---------------------------------------------------------------------------
# Async render-aware special forms
# ---------------------------------------------------------------------------

async def _arsf_if(expr, env, ctx):
    cond = await async_eval(expr[1], env, ctx)
    if cond and cond is not NIL:
        return await _arender(expr[2], env, ctx)
    if len(expr) > 3:
        return await _arender(expr[3], env, ctx)
    return ""


async def _arsf_when(expr, env, ctx):
    cond = await async_eval(expr[1], env, ctx)
    if cond and cond is not NIL:
        parts = []
        for body_expr in expr[2:]:
            parts.append(await _arender(body_expr, env, ctx))
        return "".join(parts)
    return ""


async def _arsf_cond(expr, env, ctx):
    clauses = expr[1:]
    if not clauses:
        return ""
    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 await _arender(clause[1], env, ctx)
            if isinstance(test, Keyword) and test.name == "else":
                return await _arender(clause[1], env, ctx)
            if await async_eval(test, env, ctx):
                return await _arender(clause[1], env, ctx)
    else:
        i = 0
        while i < len(clauses) - 1:
            test = clauses[i]
            result = clauses[i + 1]
            if isinstance(test, Keyword) and test.name == "else":
                return await _arender(result, env, ctx)
            if isinstance(test, Symbol) and test.name in (":else", "else"):
                return await _arender(result, env, ctx)
            if await async_eval(test, env, ctx):
                return await _arender(result, env, ctx)
            i += 2
    return ""


async def _arsf_let(expr, env, ctx):
    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] = await async_eval(binding[1], local, ctx)
        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] = await async_eval(bindings[i + 1], local, ctx)
    parts = []
    for body_expr in expr[2:]:
        parts.append(await _arender(body_expr, local, ctx))
    return "".join(parts)


async def _arsf_begin(expr, env, ctx):
    parts = []
    for sub in expr[1:]:
        parts.append(await _arender(sub, env, ctx))
    return "".join(parts)


async def _arsf_define(expr, env, ctx):
    await async_eval(expr, env, ctx)
    return ""


async def _arsf_map(expr, env, ctx):
    fn = await async_eval(expr[1], env, ctx)
    coll = await async_eval(expr[2], env, ctx)
    parts = []
    for item in coll:
        if isinstance(fn, Lambda):
            parts.append(await _arender_lambda(fn, (item,), env, ctx))
        elif callable(fn):
            parts.append(await _arender(fn(item), env, ctx))
        else:
            parts.append(await _arender(item, env, ctx))
    return "".join(parts)


async def _arsf_map_indexed(expr, env, ctx):
    fn = await async_eval(expr[1], env, ctx)
    coll = await async_eval(expr[2], env, ctx)
    parts = []
    for i, item in enumerate(coll):
        if isinstance(fn, Lambda):
            parts.append(await _arender_lambda(fn, (i, item), env, ctx))
        elif callable(fn):
            parts.append(await _arender(fn(i, item), env, ctx))
        else:
            parts.append(await _arender(item, env, ctx))
    return "".join(parts)


async def _arsf_filter(expr, env, ctx):
    result = await async_eval(expr, env, ctx)
    return await _arender(result, env, ctx)


async def _arsf_for_each(expr, env, ctx):
    fn = await async_eval(expr[1], env, ctx)
    coll = await async_eval(expr[2], env, ctx)
    parts = []
    for item in coll:
        if isinstance(fn, Lambda):
            parts.append(await _arender_lambda(fn, (item,), env, ctx))
        elif callable(fn):
            parts.append(await _arender(fn(item), env, ctx))
        else:
            parts.append(await _arender(item, env, ctx))
    return "".join(parts)


_ASYNC_RENDER_FORMS: dict[str, Any] = {
    "if": _arsf_if,
    "when": _arsf_when,
    "cond": _arsf_cond,
    "let": _arsf_let,
    "let*": _arsf_let,
    "begin": _arsf_begin,
    "do": _arsf_begin,
    "define": _arsf_define,
    "defcomp": _arsf_define,
    "defmacro": _arsf_define,
    "defhandler": _arsf_define,
    "map": _arsf_map,
    "map-indexed": _arsf_map_indexed,
    "filter": _arsf_filter,
    "for-each": _arsf_for_each,
}


# ---------------------------------------------------------------------------
# Async serialize — evaluate I/O/control flow, produce sx source (not HTML)
# ---------------------------------------------------------------------------
# Used by defhandler execution.  Fragment providers need to return sx wire
# format (s-expression source) that consuming apps parse and render client-
# side.  This mirrors the old Python handlers that used sx_call() to build
# sx strings.
#
# _aser ("async serialize") works like _arender but instead of producing
# HTML for components/tags/<>, it serializes them back to sx source with
# their arguments evaluated.


async def async_eval_to_sx(
    expr: Any,
    env: dict[str, Any],
    ctx: RequestContext | None = None,
) -> str:
    """Evaluate *expr* (resolving I/O inline) and produce sx source string.

    Unlike ``async_render`` (which produces HTML), this produces sx wire
    format suitable for fragment responses that clients render themselves.
    """
    if ctx is None:
        ctx = RequestContext()
    result = await _aser(expr, env, ctx)
    if isinstance(result, SxExpr):
        return result.source
    if result is None or result is NIL:
        return ""
    return serialize(result)


async def _aser(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any:
    """Evaluate *expr*, producing SxExpr for rendering forms, raw values
    for everything else."""
    if isinstance(expr, (int, float, bool)):
        return expr
    if isinstance(expr, str):
        return expr
    if isinstance(expr, SxExpr):
        return expr
    if expr is None or expr is NIL:
        return NIL

    if isinstance(expr, Symbol):
        name = expr.name
        if name in env:
            return env[name]
        if name in _PRIMITIVES:
            return _PRIMITIVES[name]
        if name == "true":
            return True
        if name == "false":
            return False
        if name == "nil":
            return NIL
        raise EvalError(f"Undefined symbol: {name}")

    if isinstance(expr, Keyword):
        return expr.name

    if isinstance(expr, dict):
        return {k: await _aser(v, env, ctx) for k, v in expr.items()}

    if not isinstance(expr, list):
        return expr
    if not expr:
        return []

    head = expr[0]
    if not isinstance(head, (Symbol, Lambda, list)):
        return [await _aser(x, env, ctx) for x in expr]

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

        # I/O primitives — await, return actual data
        if name in IO_PRIMITIVES:
            args, kwargs = await _parse_io_args(expr[1:], env, ctx)
            return await execute_io(name, args, kwargs, ctx)

        # <> — serialize children as sx fragment
        if name == "<>":
            return await _aser_fragment(expr[1:], env, ctx)

        # raw! — serialize
        if name == "raw!":
            return await _aser_call("raw!", expr[1:], env, ctx)

        # Component call — serialize (don't expand)
        if name.startswith("~"):
            return await _aser_call(name, expr[1:], env, ctx)

        # Serialize-mode special/HO forms (checked BEFORE HTML_TAGS
        # because some names like "map" are both HTML tags and sx forms)
        sf = _ASER_FORMS.get(name)
        if sf is not None:
            return await sf(expr, env, ctx)

        # HTML tag — serialize (don't render to HTML)
        if name in HTML_TAGS:
            return await _aser_call(name, expr[1:], env, ctx)

        # Macro expansion
        if name in env:
            val = env[name]
            if isinstance(val, Macro):
                expanded = _expand_macro(val, expr[1:], env)
                return await _aser(expanded, env, ctx)

    # Function / lambda call — evaluate (produces data, not rendering)
    fn = await async_eval(head, env, ctx)
    args = [await async_eval(a, env, ctx) for a in expr[1:]]

    if callable(fn) and not isinstance(fn, (Lambda, Component)):
        return fn(*args)
    if isinstance(fn, Lambda):
        return await _async_call_lambda(fn, args, env, ctx)
    if isinstance(fn, Component):
        # Component invoked as function — serialize the call
        return await _aser_call(f"~{fn.name}", expr[1:], env, ctx)
    raise EvalError(f"Not callable: {fn!r}")


async def _aser_fragment(children: list, env: dict, ctx: RequestContext) -> SxExpr:
    """Serialize ``(<> child1 child2 ...)`` to sx source."""
    parts: list[str] = []
    for child in children:
        result = await _aser(child, env, ctx)
        if isinstance(result, list):
            # Flatten lists (e.g. from map)
            for item in result:
                if item is not NIL and item is not None:
                    parts.append(serialize(item))
        elif result is not NIL and result is not None:
            parts.append(serialize(result))
    if not parts:
        return SxExpr("")
    return SxExpr("(<> " + " ".join(parts) + ")")


async def _aser_call(
    name: str, args: list, env: dict, ctx: RequestContext,
) -> SxExpr:
    """Serialize ``(name :key val child ...)`` — evaluate args but keep
    as sx source instead of rendering to HTML."""
    parts = [name]
    i = 0
    while i < len(args):
        arg = args[i]
        if isinstance(arg, Keyword) and i + 1 < len(args):
            val = await _aser(args[i + 1], env, ctx)
            if val is not NIL and val is not None:
                parts.append(f":{arg.name}")
                parts.append(serialize(val))
            i += 2
        else:
            result = await _aser(arg, env, ctx)
            if result is not NIL and result is not None:
                parts.append(serialize(result))
            i += 1
    return SxExpr("(" + " ".join(parts) + ")")


# ---------------------------------------------------------------------------
# Serialize-mode special forms
# ---------------------------------------------------------------------------

async def _assf_if(expr, env, ctx):
    cond = await async_eval(expr[1], env, ctx)
    if cond and cond is not NIL:
        return await _aser(expr[2], env, ctx)
    if len(expr) > 3:
        return await _aser(expr[3], env, ctx)
    return NIL


async def _assf_when(expr, env, ctx):
    cond = await async_eval(expr[1], env, ctx)
    if cond and cond is not NIL:
        result: Any = NIL
        for body_expr in expr[2:]:
            result = await _aser(body_expr, env, ctx)
        return result
    return NIL


async def _assf_let(expr, env, ctx):
    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] = await _aser(binding[1], local, ctx)
        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] = await _aser(bindings[i + 1], local, ctx)
    result: Any = NIL
    for body_expr in expr[2:]:
        result = await _aser(body_expr, local, ctx)
    return result


async def _assf_cond(expr, env, ctx):
    clauses = expr[1:]
    if not clauses:
        return NIL
    if (isinstance(clauses[0], list) and len(clauses[0]) == 2
            and not (isinstance(clauses[0][0], Symbol) and clauses[0][0].name in (
                "=", "<", ">", "<=", ">=", "!=", "and", "or"))):
        for clause in clauses:
            test = clause[0]
            if isinstance(test, Symbol) and test.name in ("else", ":else"):
                return await _aser(clause[1], env, ctx)
            if isinstance(test, Keyword) and test.name == "else":
                return await _aser(clause[1], env, ctx)
            if await async_eval(test, env, ctx):
                return await _aser(clause[1], env, ctx)
    else:
        i = 0
        while i < len(clauses) - 1:
            test = clauses[i]
            result = clauses[i + 1]
            if isinstance(test, Keyword) and test.name == "else":
                return await _aser(result, env, ctx)
            if isinstance(test, Symbol) and test.name in (":else", "else"):
                return await _aser(result, env, ctx)
            if await async_eval(test, env, ctx):
                return await _aser(result, env, ctx)
            i += 2
    return NIL


async def _assf_case(expr, env, ctx):
    match_val = await async_eval(expr[1], env, ctx)
    clauses = expr[2:]
    i = 0
    while i < len(clauses) - 1:
        test = clauses[i]
        result = clauses[i + 1]
        if isinstance(test, Keyword) and test.name == "else":
            return await _aser(result, env, ctx)
        if isinstance(test, Symbol) and test.name in (":else", "else"):
            return await _aser(result, env, ctx)
        if match_val == await async_eval(test, env, ctx):
            return await _aser(result, env, ctx)
        i += 2
    return NIL


async def _assf_begin(expr, env, ctx):
    result: Any = NIL
    for sub in expr[1:]:
        result = await _aser(sub, env, ctx)
    return result


async def _assf_define(expr, env, ctx):
    await async_eval(expr, env, ctx)
    return NIL


async def _assf_lambda(expr, env, ctx):
    return await _asf_lambda(expr, env, ctx)


async def _assf_and(expr, env, ctx):
    return await _asf_and(expr, env, ctx)


async def _assf_or(expr, env, ctx):
    return await _asf_or(expr, env, ctx)


async def _assf_quote(expr, env, ctx):
    return expr[1] if len(expr) > 1 else NIL


async def _assf_quasiquote(expr, env, ctx):
    return await _async_qq_expand(expr[1], env, ctx)


async def _assf_thread_first(expr, env, ctx):
    return await _asf_thread_first(expr, env, ctx)


async def _assf_set_bang(expr, env, ctx):
    return await _asf_set_bang(expr, env, ctx)


# ---------------------------------------------------------------------------
# Serialize-mode higher-order forms
# ---------------------------------------------------------------------------

async def _asho_ser_map(expr, env, ctx):
    fn = await async_eval(expr[1], env, ctx)
    coll = await async_eval(expr[2], env, ctx)
    results = []
    for item in coll:
        if isinstance(fn, Lambda):
            local = dict(fn.closure)
            local.update(env)
            for p, v in zip(fn.params, [item]):
                local[p] = v
            results.append(await _aser(fn.body, local, ctx))
        elif callable(fn):
            results.append(fn(item))
        else:
            raise EvalError(f"map requires callable, got {type(fn).__name__}")
    return results


async def _asho_ser_map_indexed(expr, env, ctx):
    fn = await async_eval(expr[1], env, ctx)
    coll = await async_eval(expr[2], env, ctx)
    results = []
    for i, item in enumerate(coll):
        if isinstance(fn, Lambda):
            local = dict(fn.closure)
            local.update(env)
            local[fn.params[0]] = i
            local[fn.params[1]] = item
            results.append(await _aser(fn.body, local, ctx))
        elif callable(fn):
            results.append(fn(i, item))
        else:
            raise EvalError(f"map-indexed requires callable, got {type(fn).__name__}")
    return results


async def _asho_ser_filter(expr, env, ctx):
    # filter is value-producing, delegate to eval
    return await async_eval(expr, env, ctx)


async def _asho_ser_for_each(expr, env, ctx):
    fn = await async_eval(expr[1], env, ctx)
    coll = await async_eval(expr[2], env, ctx)
    results = []
    for item in coll:
        if isinstance(fn, Lambda):
            local = dict(fn.closure)
            local.update(env)
            local[fn.params[0]] = item
            results.append(await _aser(fn.body, local, ctx))
        elif callable(fn):
            results.append(fn(item))
    return results


_ASER_FORMS: dict[str, Any] = {
    "if": _assf_if,
    "when": _assf_when,
    "cond": _assf_cond,
    "case": _assf_case,
    "and": _assf_and,
    "or": _assf_or,
    "let": _assf_let,
    "let*": _assf_let,
    "lambda": _assf_lambda,
    "fn": _assf_lambda,
    "define": _assf_define,
    "defcomp": _assf_define,
    "defmacro": _assf_define,
    "defhandler": _assf_define,
    "begin": _assf_begin,
    "do": _assf_begin,
    "quote": _assf_quote,
    "quasiquote": _assf_quasiquote,
    "->": _assf_thread_first,
    "set!": _assf_set_bang,
    "map": _asho_ser_map,
    "map-indexed": _asho_ser_map_indexed,
    "filter": _asho_ser_filter,
    "for-each": _asho_ser_for_each,
}
