import inspect
import logging
from typing import Callable

import pyarrow as pa
from fastapi import APIRouter, Request
from starlette.responses import PlainTextResponse, Response

from n6k_server.formats import arrow_response, negotiate, to_arrow

log = logging.getLogger(__name__)
router = APIRouter()


@router.get("/")
async def list_rpcs(request: Request) -> Response:
    registry = request.app.state.table_registry
    names = registry.rpc_names
    table = pa.table(
        {
            "name": pa.array(names, type=pa.utf8()),
            "path": pa.array([f"/rpc/{n}" for n in names], type=pa.utf8()),
        }
    )
    return arrow_response(table, negotiate(request))


@router.post("/{function_name}")
async def call_rpc(function_name: str, request: Request) -> Response:
    registry = request.app.state.table_registry
    spec = registry.get_rpc(function_name)
    if spec is None:
        return PlainTextResponse(f"rpc not found: {function_name}", status_code=404)

    try:
        body = await request.json()
    except Exception:
        return PlainTextResponse("invalid JSON body", status_code=400)

    try:
        args, kwargs = _bind_args(spec.fn, body, has_parent=spec.parent_table is not None)
    except TypeError as e:
        return PlainTextResponse(f"invalid arguments: {e}", status_code=400)

    if spec.parent_table is not None:
        registry.ensure_loaded(spec.parent_table)
        parent_arrow = request.app.state.cached_tables.get(spec.parent_table)
        if parent_arrow is None:
            return PlainTextResponse(f"parent table '{spec.parent_table}' not loaded", status_code=500)
        parent_df = parent_arrow.to_pandas()
        args = [parent_df] + list(args)

    try:
        result = spec.fn(*args, **kwargs)
    except Exception as e:
        log.exception("RPC '%s' execution error", function_name)
        return PlainTextResponse(f"server error: {e}", status_code=500)

    arrow_table = to_arrow(result)

    log.info("POST /rpc/%s — %d rows", function_name, arrow_table.num_rows)
    return arrow_response(arrow_table, negotiate(request))


def _bind_args(fn: Callable, body, *, has_parent: bool) -> tuple[list, dict]:
    """Map JSON body to function arguments.

    For data-attached RPCs, the first parameter (parent df) is excluded
    from validation since it's injected by the handler.
    """
    sig = inspect.signature(fn)
    params = list(sig.parameters.values())

    if has_parent and params:
        params = params[1:]

    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)}, " f"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__}")
