"""Async endpoint builders for CRUD operations."""

from typing import TYPE_CHECKING, Annotated, cast

from fastapi import Depends, Query, status
from pydantic import BaseModel
from sqlmodel import SQLModel, select
from sqlmodel.ext.asyncio.session import AsyncSession

from auen._async_endpoints_bulk_ops import (
    add_async_bulk_create,
    add_async_bulk_delete,
    add_async_bulk_update,
)
from auen._async_endpoints_common import (
    _build_nested_create_schema,
    _build_nested_update_schema,
    _op_kwargs,
    _rename_path_param,
    _resolve_relation_fk_field,
    _set_param_annotations,
)
from auen._async_endpoints_nested_ops import (
    add_async_nested_create,
    add_async_nested_update,
)
from auen._endpoints import (
    RouterContext,
    _build_filter_model,
    _build_page_model,
    _make_user_dep,
)
from auen._query import apply_filters, apply_sorting
from auen.config import FilterConfig, Operation
from auen.exceptions import ForbiddenError, NotFoundError
from auen.types import ListResponse, PKValue, ResponseModel, User

__all__ = [
    "_build_nested_create_schema",
    "_build_nested_update_schema",
    "_resolve_relation_fk_field",
    "add_async_bulk_create",
    "add_async_bulk_delete",
    "add_async_bulk_update",
    "add_async_create",
    "add_async_delete",
    "add_async_list",
    "add_async_nested_create",
    "add_async_nested_update",
    "add_async_read",
    "add_async_update",
]


def add_async_create(ctx: RouterContext) -> None:
    """Register the async CREATE endpoint."""
    create_schema = ctx.schemas.create
    read_schema = ctx.schemas.read
    user_dep = _make_user_dep(ctx.auth, Operation.CREATE)
    model = ctx.model
    repo_factory = ctx.repository_factory

    @ctx.router.post(
        "/",
        response_model=read_schema,
        status_code=status.HTTP_201_CREATED,
        **_op_kwargs(ctx, Operation.CREATE),
    )
    async def create_resource(
        obj_in: BaseModel,
        session: Annotated[AsyncSession, Depends(ctx.session_dep)],
        user: Annotated[User, user_dep],
    ) -> SQLModel:
        pol = ctx.policy
        if not pol.can_create(user, obj_in):
            raise ForbiddenError()
        hooks = ctx.hooks
        if hooks.before_create:
            await hooks.before_create(session, obj_in, user)
        repo = repo_factory(model)
        db_obj = await repo.create(session, obj_in=obj_in)
        if hooks.after_create:
            await hooks.after_create(session, db_obj, user)
        return db_obj

    _set_param_annotations(create_resource, {"obj_in": create_schema})


async def _list_core(
    *,
    ctx: RouterContext,
    session: AsyncSession,
    user: User,
    offset: int,
    limit: int,
    sort: str | None,
    filter_params: BaseModel | None,
) -> ListResponse:
    """Shared LIST logic for all pagination styles."""
    model = ctx.model
    filters = ctx.filters
    pol = ctx.policy
    pg = ctx.pagination

    query = select(model)
    query = pol.filter_list_query(user, query)
    if filters and filter_params is not None:
        query = apply_filters(
            query,
            model,
            filters,
            filter_params.model_dump(exclude_none=True),
        )
    query = apply_sorting(query, model, filters, sort)
    repo = ctx.repository_factory(model)
    items = list(await repo.list(session, query=query, offset=offset, limit=limit))
    if pg.include_total:
        total = await repo.count(session, query=query)
        return {"items": items, "total": total, "limit": limit, "offset": offset}
    return items


def add_async_list(ctx: RouterContext) -> None:
    """Register the async LIST endpoint."""
    read_schema = ctx.schemas.read
    user_dep = _make_user_dep(ctx.auth, Operation.LIST)
    filters = ctx.filters
    pg = ctx.pagination

    response_model: ResponseModel
    if TYPE_CHECKING:
        response_model = cast(ResponseModel, list[BaseModel])
    else:
        response_model = cast(ResponseModel, list[read_schema])
    if pg.include_total:
        response_model = _build_page_model(read_schema)

    _sort_query = Query(default=None)

    # Build filter model: always present (empty if no filters configured)
    effective_filters = filters or FilterConfig()
    filter_model = _build_filter_model(ctx.model, effective_filters)
    filter_dep = Depends(filter_model)

    route_kwargs = _op_kwargs(ctx, Operation.LIST)

    if pg.style == "page":
        _page_query = Query(default=1, ge=1)
        _size_query = Query(default=pg.default_limit, ge=1, le=pg.max_limit)

        @ctx.router.get("/", response_model=response_model, **route_kwargs)
        async def list_resources(
            session: Annotated[AsyncSession, Depends(ctx.session_dep)],
            user: Annotated[User, user_dep],
            filter_params: Annotated[BaseModel, filter_dep],
            page: int = _page_query,
            size: int = _size_query,
            sort: str | None = _sort_query,
        ) -> ListResponse:
            offset = (page - 1) * size
            return await _list_core(
                ctx=ctx,
                session=session,
                user=user,
                offset=offset,
                limit=size,
                sort=sort,
                filter_params=filter_params if filters else None,
            )

    else:
        _offset_query = Query(default=0, ge=0)
        _limit_query = Query(default=pg.default_limit, ge=1, le=pg.max_limit)

        @ctx.router.get("/", response_model=response_model, **route_kwargs)
        async def list_resources(
            session: Annotated[AsyncSession, Depends(ctx.session_dep)],
            user: Annotated[User, user_dep],
            filter_params: Annotated[BaseModel, filter_dep],
            offset: int = _offset_query,
            limit: int = _limit_query,
            sort: str | None = _sort_query,
        ) -> ListResponse:
            return await _list_core(
                ctx=ctx,
                session=session,
                user=user,
                offset=offset,
                limit=limit,
                sort=sort,
                filter_params=filter_params if filters else None,
            )


def add_async_read(ctx: RouterContext) -> None:
    """Register the async READ endpoint."""
    read_schema = ctx.schemas.read
    user_dep = _make_user_dep(ctx.auth, Operation.READ)
    model = ctx.model
    model_name = ctx.model_name
    repo_factory = ctx.repository_factory
    pol = ctx.policy
    pk_name = ctx.pk_name
    pk_type = ctx.pk_type

    @ctx.router.get(
        "/{" + pk_name + "}",
        response_model=read_schema,
        **_op_kwargs(ctx, Operation.READ),
    )
    async def read_resource(
        session: Annotated[AsyncSession, Depends(ctx.session_dep)],
        user: Annotated[User, user_dep],
        **path_params: PKValue,
    ) -> SQLModel:
        pk_value = path_params[pk_name]
        repo = repo_factory(model)
        db_obj = await repo.get(session, pk_value)
        if db_obj is None:
            raise NotFoundError(model_name, pk_name, pk_value)
        if not pol.can_read(user, db_obj):
            raise ForbiddenError()
        return db_obj

    _rename_path_param(read_resource, "path_params", pk_name, pk_type)


def add_async_update(ctx: RouterContext) -> None:
    """Register the async UPDATE endpoint."""
    update_schema = ctx.schemas.update
    read_schema = ctx.schemas.read
    user_dep = _make_user_dep(ctx.auth, Operation.UPDATE)
    model = ctx.model
    model_name = ctx.model_name
    repo_factory = ctx.repository_factory
    pol = ctx.policy
    pk_name = ctx.pk_name
    pk_type = ctx.pk_type

    @ctx.router.patch(
        "/{" + pk_name + "}",
        response_model=read_schema,
        **_op_kwargs(ctx, Operation.UPDATE),
    )
    async def update_resource(
        obj_in: BaseModel,
        session: Annotated[AsyncSession, Depends(ctx.session_dep)],
        user: Annotated[User, user_dep],
        **path_params: PKValue,
    ) -> SQLModel:
        pk_value = path_params[pk_name]
        repo = repo_factory(model)
        db_obj = await repo.get(session, pk_value)
        if db_obj is None:
            raise NotFoundError(model_name, pk_name, pk_value)
        if not pol.can_update(user, db_obj, obj_in):
            raise ForbiddenError()
        hooks = ctx.hooks
        if hooks.before_update:
            await hooks.before_update(session, db_obj, user)
        result = await repo.update(session, db_obj=db_obj, obj_in=obj_in)
        if hooks.after_update:
            await hooks.after_update(session, result, user)
        return result

    _rename_path_param(update_resource, "path_params", pk_name, pk_type)
    _set_param_annotations(update_resource, {"obj_in": update_schema})


def add_async_delete(ctx: RouterContext) -> None:
    """Register the async DELETE endpoint."""
    read_schema = ctx.schemas.read
    user_dep = _make_user_dep(ctx.auth, Operation.DELETE)
    model = ctx.model
    model_name = ctx.model_name
    repo_factory = ctx.repository_factory
    pol = ctx.policy
    pk_name = ctx.pk_name
    pk_type = ctx.pk_type

    @ctx.router.delete(
        "/{" + pk_name + "}",
        response_model=read_schema,
        **_op_kwargs(ctx, Operation.DELETE),
    )
    async def delete_resource(
        session: Annotated[AsyncSession, Depends(ctx.session_dep)],
        user: Annotated[User, user_dep],
        **path_params: PKValue,
    ) -> SQLModel:
        pk_value = path_params[pk_name]
        repo = repo_factory(model)
        db_obj = await repo.get(session, pk_value)
        if db_obj is None:
            raise NotFoundError(model_name, pk_name, pk_value)
        if not pol.can_delete(user, db_obj):
            raise ForbiddenError()
        hooks = ctx.hooks
        if hooks.before_delete:
            await hooks.before_delete(session, db_obj, user)
        await repo.delete(session, db_obj=db_obj)
        if hooks.after_delete:
            await hooks.after_delete(session, db_obj, user)
        return db_obj

    _rename_path_param(delete_resource, "path_params", pk_name, pk_type)
