"""Shared helpers for async endpoint registration."""

import inspect
from collections.abc import Callable
from typing import TypedDict

from pydantic import BaseModel
from sqlalchemy import inspect as sa_inspect
from sqlalchemy.orm import RelationshipDirection
from sqlmodel import SQLModel

from auen._endpoints import RouterContext
from auen.config import Operation
from auen.exceptions import ConfigurationError
from auen.schemas import derive_schemas


class OpKwargs(TypedDict):
    include_in_schema: bool
    operation_id: str


def _set_param_annotations(
    func: Callable[..., object], annotations: dict[str, object]
) -> None:
    """Update parameter annotations and rebuild __signature__ to match."""
    func.__annotations__.update(annotations)
    sig = inspect.signature(func)
    new_params = []
    for p in sig.parameters.values():
        if p.name in annotations:
            new_params.append(p.replace(annotation=annotations[p.name]))
        else:
            new_params.append(p)
    func.__signature__ = sig.replace(parameters=new_params)  # type: ignore[attr-defined]


def _rename_path_param(
    func: Callable[..., object], old_name: str, new_name: str, annotation: object
) -> None:
    """Rename a function parameter so FastAPI uses `new_name` as the path param.

    Replaces a VAR_KEYWORD (**kwargs) parameter with a named POSITIONAL_OR_KEYWORD
    parameter in the function's ``__signature__``. The actual function must use
    ``**kwargs`` to receive the value at runtime.
    """
    sig = getattr(func, "__signature__", None) or inspect.signature(func)
    new_params: list[inspect.Parameter] = []
    for p in sig.parameters.values():
        if p.name == old_name:
            new_params.append(
                inspect.Parameter(
                    new_name,
                    kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
                    annotation=annotation,
                )
            )
        else:
            new_params.append(p)
    func.__signature__ = sig.replace(parameters=new_params)  # type: ignore[attr-defined]
    func.__annotations__.pop(old_name, None)
    func.__annotations__[new_name] = annotation


def _op_kwargs(ctx: RouterContext, op: Operation) -> OpKwargs:
    """Build shared route kwargs for include_in_schema and operation_id."""
    return {
        "include_in_schema": op in ctx.include_in_schema,
        "operation_id": f"{ctx.operation_id_prefix}_{op.value}",
    }


def _resolve_relation_fk_field(
    model: type[SQLModel],
    relation_name: str,
    related_model: type[SQLModel],
) -> str:
    mapper = sa_inspect(model)
    if relation_name not in mapper.relationships:
        msg = f"Unknown relationship '{relation_name}' on model {model.__name__}"
        raise ConfigurationError(msg)
    relation = mapper.relationships[relation_name]
    if relation.direction != RelationshipDirection.MANYTOONE:
        msg = (
            f"Nested writes support only MANYTOONE relations. "
            f"{model.__name__}.{relation_name} is {relation.direction.name}"
        )
        raise ConfigurationError(msg)
    if relation.mapper.class_ is not related_model:
        msg = (
            f"Nested relation '{relation_name}' model mismatch: "
            f"{relation.mapper.class_.__name__} != {related_model.__name__}"
        )
        raise ConfigurationError(msg)
    if len(relation.local_columns) != 1:
        msg = (
            f"Nested writes require exactly one local FK column for "
            f"{model.__name__}.{relation_name}"
        )
        raise ConfigurationError(msg)
    return next(iter(relation.local_columns)).name


def _build_nested_update_schema(ctx: RouterContext) -> type[BaseModel]:
    if ctx.schemas.nested_update is not None:
        return ctx.schemas.nested_update
    nested_schema = derive_schemas(
        ctx.model,
        nested_writes=ctx.nested_writes,
    ).nested_update
    assert nested_schema is not None
    return nested_schema


def _build_nested_create_schema(ctx: RouterContext) -> type[BaseModel]:
    if ctx.schemas.nested_create is not None:
        return ctx.schemas.nested_create
    nested_schema = derive_schemas(
        ctx.model,
        nested_writes=ctx.nested_writes,
    ).nested_create
    assert nested_schema is not None
    return nested_schema
