import inspect
import os
from pathlib import Path
from typing import Any, Callable

from fastapi import APIRouter, FastAPI, Request
from starlette.responses import HTMLResponse, JSONResponse, Response

DEV_MODE = os.environ.get("N6K_DEV") == "1"

HTML_TEMPLATE = """\
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{asset_prefix}/styles.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="{script_src}"></script>
{dev_script}
</body>
</html>"""

BUILD_DIR = Path("build")

asset_router = APIRouter()


@asset_router.get("/styles.css")
async def serve_styles():
    css_file = BUILD_DIR / "styles.css"
    if not css_file.exists():
        return Response("not found", status_code=404)
    return Response(content=css_file.read_text(), media_type="text/css")


@asset_router.get("/pages/{path:path}")
async def serve_page_asset(path: str):
    asset = BUILD_DIR / path
    if not asset.exists() or not asset.is_file():
        return Response("not found", status_code=404)
    return Response(
        content=asset.read_text(),
        media_type="application/javascript",
    )


def wrap_html(asset_prefix: str, script_src: str) -> str:
    dev_script = ""
    if DEV_MODE:
        from nextpy_server.dev import dev_script as make_dev_script

        dev_script = make_dev_script(asset_prefix)
    return HTML_TEMPLATE.format(
        asset_prefix=asset_prefix, script_src=script_src, dev_script=dev_script
    )


def bind(app: FastAPI, name: str, *, default: Any = None):
    app.state.bindings[name] = default


def page(app: FastAPI, path: str, snippet: str):
    slug = path.strip("/") or "index"
    app.state.pages[slug] = snippet
    _register_shared_routes(app)

    prefix = app.state.asset_prefix
    script_src = f"{prefix}/pages/{slug}.js"

    @app.get(path, response_class=HTMLResponse)
    async def serve_page():
        return HTMLResponse(wrap_html(prefix, script_src))

    @app.post(path)
    async def handle_action(request: Request):
        page_actions = app.state.actions.get(slug, {})
        body = await request.json()
        action_name = body.pop("_action", None)
        if not action_name or action_name not in page_actions:
            return JSONResponse({"ok": False, "error": f"unknown action: {action_name}"}, status_code=404)
        fn = page_actions[action_name]
        args, kwargs = _bind_args(fn, body)
        result = fn(*args, **kwargs)
        return JSONResponse({"ok": True, "data": result})


def _register_shared_routes(app: FastAPI):
    if getattr(app.state, "_asset_routes_registered", False):
        return
    app.state._asset_routes_registered = True

    app.include_router(asset_router, prefix=app.state.asset_prefix)

    if DEV_MODE:
        from nextpy_server.dev import register_dev

        register_dev(app)


def _bind_args(fn: Callable, body: dict | list | None) -> tuple[list, dict]:
    """Map JSON body to function arguments via inspect.signature."""
    sig = inspect.signature(fn)
    params = list(sig.parameters.values())

    if body is None or body == {}:
        return [], {}

    if isinstance(body, list):
        if len(body) > len(params):
            raise TypeError(f"too many positional arguments: got {len(body)}, expected at most {len(params)}")
        return body, {}

    if isinstance(body, dict):
        valid_names = {p.name for p in params}
        unknown = set(body.keys()) - valid_names
        if unknown:
            raise TypeError(f"unknown parameters: {', '.join(sorted(unknown))}")
        return [], body

    raise TypeError(f"body must be JSON object or array, got {type(body).__name__}")
