"""GraphSX URL router — evaluate s-expression URLs.

Handles URLs like /(language.(doc.introduction)) by:
1. Delegating to the bootstrapped spec (router.sx → prepare_url_expr):
   - Dots → spaces (URL-safe whitespace encoding)
   - Parse as SX expression
   - Auto-quote unknowns (symbols not in env → strings)
2. Evaluating the prepared expression against page functions
3. Wrapping the result in (~layouts/doc :path "..." content)
4. Returning full page or OOB response

The URL evaluation logic lives in the SX spec (shared/sx/ref/router.sx)
and is bootstrapped to Python (sx_ref.py) and JavaScript (sx-browser.js).
This handler is generic infrastructure — all routing semantics are in SX.

Special cases:
- "/" → home page
- Streaming pages → delegate to existing PageDef infrastructure
- Direct component URLs: /(~component-name) → render component
"""
from __future__ import annotations

import logging
import re
from typing import Any
from urllib.parse import unquote

logger = logging.getLogger("sx.router")


# ---------------------------------------------------------------------------
# Streaming detection (host-level concern, not in spec)
# ---------------------------------------------------------------------------

_STREAMING_PAGES = {
    # URL slug → defpage name for streaming pages
    "streaming": "streaming-demo",
}


def _is_streaming_url(expr: Any) -> str | None:
    """Check if expression resolves to a streaming page.

    Returns the defpage name if streaming, None otherwise.
    Detects: (geography (isomorphism "streaming"))
    """
    from shared.sx.types import Symbol

    # Check for (geography (isomorphism "streaming"))
    if not isinstance(expr, list) or len(expr) < 2:
        return None

    # Walk nested structure to find isomorphism call
    def _find_slug(e: Any) -> str | None:
        if not isinstance(e, list) or not e:
            return None
        head = e[0]
        if isinstance(head, Symbol) and head.name == "isomorphism":
            for arg in e[1:]:
                if isinstance(arg, str) and arg in _STREAMING_PAGES:
                    return _STREAMING_PAGES[arg]
                if isinstance(arg, Symbol) and arg.name in _STREAMING_PAGES:
                    return _STREAMING_PAGES[arg.name]
        # Recurse into nested calls
        for arg in e[1:]:
            result = _find_slug(arg)
            if result:
                return result
        return None

    return _find_slug(expr)


# ---------------------------------------------------------------------------
# Main eval handler
# ---------------------------------------------------------------------------

async def eval_sx_url(raw_path: str) -> Any:
    """Evaluate an SX URL and return the response.

    This is the main entry point for the catch-all route handler.
    Returns a Quart Response object, or None if the path isn't an SX URL.

    URL parsing and auto-quoting are delegated to the bootstrapped spec
    functions (prepare_url_expr from router.sx). This handler provides
    only the host-level concerns: building the env, async eval, response
    formatting, streaming detection.
    """
    from quart import make_response, Response
    from shared.sx.jinja_bridge import get_component_env, _get_request_context
    from shared.sx.pages import get_page, get_page_helpers, _eval_slot
    from shared.sx.types import Symbol, Keyword
    from shared.sx.helpers import full_page_sx, oob_page_sx, sx_response, sx_page
    from shared.sx.page import get_template_context
    from shared.browser.app.utils.htmx import is_htmx_request
    from shared.sx.pages import prepare_url_expr

    path = unquote(raw_path).strip()

    # Home page
    if path == "/":
        expr = [Symbol("home")]
    else:
        # SX URLs: /(expr) or /~component
        if not (path.startswith("/(") or path.startswith("/~")):
            return None  # not an SX URL — let other handlers try

        # Build env for auto-quoting: components + page helpers
        env = dict(get_component_env())
        env.update(get_page_helpers("sx"))

        # Use the bootstrapped spec: parse URL, auto-quote unknowns
        expr = prepare_url_expr(path[1:], env)  # strip leading /

        # Bare symbol (e.g. /~comp) → wrap in list for eval
        if isinstance(expr, Symbol):
            expr = [expr]

        if not expr:
            return None

    # Check for streaming page BEFORE eval
    streaming_page = _is_streaming_url(expr)
    if streaming_page:
        page_def = get_page("sx", streaming_page)
        if page_def:
            from shared.sx.pages import execute_page_streaming, execute_page_streaming_oob
            if is_htmx_request():
                gen = await execute_page_streaming_oob(page_def, "sx")
                return Response(gen, content_type="text/sx; charset=utf-8")
            gen = await execute_page_streaming(page_def, "sx")
            return Response(gen, content_type="text/html; charset=utf-8")

    # Build env if not already built (home case)
    if path == "/":
        env = dict(get_component_env())
        env.update(get_page_helpers("sx"))

    ctx = _get_request_context()

    # Nav hrefs use /sx/ prefix — reconstruct the full path for nav matching
    path_str = f"/sx{raw_path}" if raw_path != "/" else "/sx/"

    # Component calls go straight to _eval_slot (aser handles expansion).
    # Page function calls need async_eval first (routing + data fetching).
    head = expr[0] if isinstance(expr, list) and expr else None
    is_component_call = isinstance(head, Symbol) and head.name.startswith("~")

    if is_component_call:
        page_ast = expr
    else:
        import os
        use_ocaml = os.environ.get("SX_USE_OCAML") == "1"

        if use_ocaml:
            # OCaml kernel — the universal evaluator
            try:
                from shared.sx.ocaml_bridge import get_bridge
                from shared.sx.parser import serialize, parse_all

                bridge = await get_bridge()
                sx_text = serialize(expr)
                ocaml_ctx = {"_helper_service": "sx"}
                result_text = await bridge.eval(sx_text, ctx=ocaml_ctx)
                if result_text:
                    parsed = parse_all(result_text)
                    page_ast = parsed[0] if parsed else []
                else:
                    page_ast = []
            except Exception as e:
                logger.error("SX URL page-fn eval (OCaml) failed for %s: %s",
                             raw_path, e, exc_info=True)
                return None
        else:
            # Python fallback
            from shared.sx.async_eval import async_eval

            try:
                page_ast = await async_eval(expr, env, ctx)
            except Exception as e:
                logger.error("SX URL page-fn eval failed for %s: %s",
                             raw_path, e, exc_info=True)
                return None

        if page_ast is None:
            page_ast = []

    wrapped_ast = [
        Symbol("~layouts/doc"), Keyword("path"), path_str,
        page_ast,
    ]

    try:
        import os as _os
        if _os.environ.get("SX_USE_OCAML") == "1":
            from shared.sx.ocaml_bridge import get_bridge
            from shared.sx.parser import serialize
            from shared.sx.types import SxExpr
            bridge = await get_bridge()
            ocaml_ctx = {"_helper_service": "sx"}

            if is_htmx_request():
                # HTMX: render everything to HTML server-side.
                # aser_slot left content component calls unexpanded (browser
                # doesn't have their definitions). bridge.render expands all
                # server-affinity components — ~layouts/doc, content pages,
                # syntax highlighting — before the response is sent.
                # handle-html-response in engine.sx processes sx-swap-oob
                # attributes (filter/aside/root-menu/sx-nav) as OOB swaps,
                # then puts the remaining #main-panel section in the target.
                oob_ast = [
                    Symbol("~shared:layout/oob-sx"),
                    Keyword("content"), wrapped_ast,
                ]
                html = await bridge.render(serialize(oob_ast), ctx=ocaml_ctx)
                return await make_response(html, 200,
                    {"Content-Type": "text/html; charset=utf-8"})
            else:
                # Full page: single OCaml call — aser-slot + shell render
                full_ast = [
                    Symbol("~shared:layout/app-body"),
                    Keyword("content"), wrapped_ast,
                ]
                page_source = serialize(full_ast)

                # Pre-compute shell kwargs in Python
                import time as _time
                _t0 = _time.monotonic()
                from shared.sx.helpers import _build_shell_kwargs
                tctx = await get_template_context()
                _t1 = _time.monotonic()
                shell_kwargs = await _build_shell_kwargs(tctx, page_source)
                _t2 = _time.monotonic()
                html = await bridge.sx_page_full(
                    page_source, shell_kwargs, ctx=ocaml_ctx)
                _t3 = _time.monotonic()
                logger.info("[sx-page-full-py] %s ctx=%.3fs kwargs=%.3fs ocaml=%.3fs total=%.3fs",
                    raw_path, _t1-_t0, _t2-_t1, _t3-_t2, _t3-_t0)
                return await make_response(html, 200)
        else:
            content_sx = await _eval_slot(wrapped_ast, env, ctx)
    except Exception as e:
        logger.error("SX URL render failed for %s: %s", raw_path, e, exc_info=True)
        return None

    # Return response (Python path — SX_USE_OCAML=0 only)
    if is_htmx_request():
        from shared.sx.async_eval import async_render
        from shared.sx.jinja_bridge import get_component_env
        oob_ast = [
            Symbol("~shared:layout/oob-sx"),
            Keyword("content"), wrapped_ast,
        ]
        env2 = dict(get_component_env())
        html = await async_render(oob_ast, env2, ctx)
        return await make_response(html, 200,
            {"Content-Type": "text/html; charset=utf-8"})
    else:
        tctx = await get_template_context()
        html = await full_page_sx(tctx, header_rows="", content=content_sx)
        return await make_response(html, 200)


# ---------------------------------------------------------------------------
# Old URL → SX URL redirect patterns
# ---------------------------------------------------------------------------

_REDIRECT_PATTERNS = [
    # --- API endpoint redirects (most specific first) ---

    # Reference API: /geography/hypermedia/reference/api/item/<id>
    (re.compile(r"^/geography/hypermedia/reference/api/item/(.+?)/?$"),
     lambda m: f"/sx/(geography.(hypermedia.(reference.(api.(item.{m.group(1)})))))"),
    # Reference API: /geography/hypermedia/reference/api/<name>
    (re.compile(r"^/geography/hypermedia/reference/api/(.+?)/?$"),
     lambda m: f"/sx/(geography.(hypermedia.(reference.(api.{m.group(1)}))))"),

    # Example API: sub-paths with params
    (re.compile(r"^/geography/hypermedia/examples/api/editrow/(.+?)/cancel/?$"),
     lambda m: f"/sx/(geography.(hypermedia.(example.(api.(editrow-cancel.{m.group(1)})))))"),
    (re.compile(r"^/geography/hypermedia/examples/api/editrow/(.+?)/?$"),
     lambda m: f"/sx/(geography.(hypermedia.(example.(api.(editrow.{m.group(1)})))))"),
    (re.compile(r"^/geography/hypermedia/examples/api/delete/(.+?)/?$"),
     lambda m: f"/sx/(geography.(hypermedia.(example.(api.(delete.{m.group(1)})))))"),
    (re.compile(r"^/geography/hypermedia/examples/api/tabs/(.+?)/?$"),
     lambda m: f"/sx/(geography.(hypermedia.(example.(api.(tabs.{m.group(1)})))))"),
    # Example API: sub-paths collapsed to hyphens
    (re.compile(r"^/geography/hypermedia/examples/api/edit/cancel/?$"),
     "/sx/(geography.(hypermedia.(example.(api.edit-cancel))))"),
    (re.compile(r"^/geography/hypermedia/examples/api/progress/start/?$"),
     "/sx/(geography.(hypermedia.(example.(api.progress-start))))"),
    (re.compile(r"^/geography/hypermedia/examples/api/progress/status/?$"),
     "/sx/(geography.(hypermedia.(example.(api.progress-status))))"),
    (re.compile(r"^/geography/hypermedia/examples/api/validate/submit/?$"),
     "/sx/(geography.(hypermedia.(example.(api.validate-submit))))"),
    (re.compile(r"^/geography/hypermedia/examples/api/dialog/close/?$"),
     "/sx/(geography.(hypermedia.(example.(api.dialog-close))))"),
    (re.compile(r"^/geography/hypermedia/examples/api/putpatch/edit-all/?$"),
     "/sx/(geography.(hypermedia.(example.(api.putpatch-edit-all))))"),
    (re.compile(r"^/geography/hypermedia/examples/api/putpatch/cancel/?$"),
     "/sx/(geography.(hypermedia.(example.(api.putpatch-cancel))))"),
    # Example API: simple names
    (re.compile(r"^/geography/hypermedia/examples/api/(.+?)/?$"),
     lambda m: f"/sx/(geography.(hypermedia.(example.(api.{m.group(1)}))))"),

    # Reactive API: sub-paths collapsed
    (re.compile(r"^/geography/reactive/api/search/(.+?)/?$"),
     lambda m: f"/sx/(geography.(reactive.(api.search-{m.group(1)})))"),
    (re.compile(r"^/geography/reactive/api/(.+?)/?$"),
     lambda m: f"/sx/(geography.(reactive.(api.{m.group(1)})))"),

    # --- Page redirects ---

    # More specific first
    (re.compile(r"^/language/specs/explore/(.+?)/?$"),
     lambda m: f"/sx/(language.(spec.(explore.{m.group(1)})))"),
    (re.compile(r"^/language/docs/(.+?)/?$"),
     lambda m: f"/sx/(language.(doc.{m.group(1)}))"),
    (re.compile(r"^/language/docs/?$"),
     "/sx/(language.(doc))"),
    (re.compile(r"^/language/specs/(.+?)/?$"),
     lambda m: f"/sx/(language.(spec.{m.group(1)}))"),
    (re.compile(r"^/language/bootstrappers/page-helpers/?$"),
     "/sx/(language.(bootstrapper.page-helpers))"),
    (re.compile(r"^/language/bootstrappers/(.+?)/?$"),
     lambda m: f"/sx/(language.(bootstrapper.{m.group(1)}))"),
    (re.compile(r"^/language/testing/(.+?)/?$"),
     lambda m: f"/sx/(language.(test.{m.group(1)}))"),
    (re.compile(r"^/geography/hypermedia/reference/attributes/(.+?)/?$"),
     lambda m: f"/sx/(geography.(hypermedia.(reference-detail.attributes.{m.group(1)})))"),
    (re.compile(r"^/geography/hypermedia/reference/headers/(.+?)/?$"),
     lambda m: f"/sx/(geography.(hypermedia.(reference-detail.headers.{m.group(1)})))"),
    (re.compile(r"^/geography/hypermedia/reference/events/(.+?)/?$"),
     lambda m: f"/sx/(geography.(hypermedia.(reference-detail.events.{m.group(1)})))"),
    (re.compile(r"^/geography/hypermedia/reference/(.+?)/?$"),
     lambda m: f"/sx/(geography.(hypermedia.(reference.{m.group(1)})))"),
    (re.compile(r"^/geography/hypermedia/examples/(.+?)/?$"),
     lambda m: f"/sx/(geography.(hypermedia.(example.{m.group(1)})))"),
    (re.compile(r"^/geography/hypermedia/reference/?$"),
     "/sx/(geography.(hypermedia.(reference)))"),
    (re.compile(r"^/geography/hypermedia/examples/?$"),
     "/sx/(geography.(hypermedia.(example)))"),
    (re.compile(r"^/geography/reactive/(.+?)/?$"),
     lambda m: f"/sx/(geography.(reactive.{m.group(1)}))"),
    (re.compile(r"^/geography/isomorphism/(.+?)/?$"),
     lambda m: f"/sx/(geography.(isomorphism.{m.group(1)}))"),
    (re.compile(r"^/geography/cek/(.+?)/?$"),
     lambda m: f"/sx/(geography.(cek.{m.group(1)}))"),
    (re.compile(r"^/geography/spreads/?$"),
     "/sx/(geography.(spreads))"),
    (re.compile(r"^/geography/marshes/?$"),
     "/sx/(geography.(marshes))"),
    (re.compile(r"^/applications/cssx/(.+?)/?$"),
     lambda m: f"/sx/(applications.(cssx.{m.group(1)}))"),
    (re.compile(r"^/applications/protocols/(.+?)/?$"),
     lambda m: f"/sx/(applications.(protocol.{m.group(1)}))"),
    (re.compile(r"^/etc/essays/(.+?)/?$"),
     lambda m: f"/sx/(etc.(essay.{m.group(1)}))"),
    (re.compile(r"^/etc/philosophy/(.+?)/?$"),
     lambda m: f"/sx/(etc.(philosophy.{m.group(1)}))"),
    (re.compile(r"^/etc/plans/(.+?)/?$"),
     lambda m: f"/sx/(etc.(plan.{m.group(1)}))"),
    # Section indices
    (re.compile(r"^/language/docs/?$"), "/sx/(language.(doc))"),
    (re.compile(r"^/language/specs/?$"), "/sx/(language.(spec))"),
    (re.compile(r"^/language/bootstrappers/?$"), "/sx/(language.(bootstrapper))"),
    (re.compile(r"^/language/testing/?$"), "/sx/(language.(test))"),
    (re.compile(r"^/language/?$"), "/sx/(language)"),
    (re.compile(r"^/geography/hypermedia/?$"), "/sx/(geography.(hypermedia))"),
    (re.compile(r"^/geography/reactive/?$"), "/sx/(geography.(reactive))"),
    (re.compile(r"^/geography/isomorphism/?$"), "/sx/(geography.(isomorphism))"),
    (re.compile(r"^/geography/cek/?$"), "/sx/(geography.(cek))"),
    (re.compile(r"^/geography/?$"), "/sx/(geography)"),
    (re.compile(r"^/applications/cssx/?$"), "/sx/(applications.(cssx))"),
    (re.compile(r"^/applications/protocols/?$"), "/sx/(applications.(protocol))"),
    (re.compile(r"^/applications/?$"), "/sx/(applications)"),
    (re.compile(r"^/etc/essays/?$"), "/sx/(etc.(essay))"),
    (re.compile(r"^/etc/philosophy/?$"), "/sx/(etc.(philosophy))"),
    (re.compile(r"^/etc/plans/?$"), "/sx/(etc.(plan))"),
    (re.compile(r"^/etc/?$"), "/sx/(etc)"),
]


def redirect_old_url(path: str) -> str | None:
    """Check if an old-style path should redirect to an SX URL.

    Returns the new SX URL if a redirect is needed, None otherwise.
    """
    for pattern, replacement in _REDIRECT_PATTERNS:
        m = pattern.match(path)
        if m:
            if callable(replacement):
                return replacement(m)
            return replacement
    return None
