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

from collections.abc import Callable
from datetime import datetime, timezone
from types import GenericAlias
from typing import TYPE_CHECKING, Annotated, TypedDict, cast

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

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 ConfigurationError, ForbiddenError, NotFoundError
from auen.types import (
    ListResponse,
    ModelFieldDefinition,
    PKValue,
    ResponseModel,
    SelectQuery,
    TModel,
    User,
)


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


def _set_param_annotations(
    func: Callable[..., object], annotations: dict[str, object]
) -> None:
    func.__annotations__.update(annotations)


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 _is_soft_deleted(ctx: RouterContext, db_obj: SQLModel) -> bool:
    """Check if a row is soft-deleted."""
    if ctx.soft_delete is None:
        return False
    return getattr(db_obj, ctx.soft_delete.field, None) is not None


def _apply_soft_delete_filter(
    ctx: RouterContext, query: SelectQuery[TModel]
) -> SelectQuery[TModel]:
    """Exclude soft-deleted rows from a query."""
    if ctx.soft_delete is None:
        return query
    col = getattr(ctx.model, ctx.soft_delete.field, None)
    if col is not None:
        query = query.where(col.is_(None))
    return query


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 = _apply_soft_delete_filter(ctx, query)
    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)
    model = ctx.model
    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(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,
            )

    elif pg.style == "limit_offset":
        _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,
            )

    else:
        msg = f"Unsupported pagination style: {pg.style}"
        raise ConfigurationError(msg)


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(
        "/{item_id}",
        response_model=read_schema,
        **_op_kwargs(ctx, Operation.READ),
    )
    async def read_resource(
        item_id: PKValue,
        session: Annotated[AsyncSession, Depends(ctx.session_dep)],
        user: Annotated[User, user_dep],
    ) -> SQLModel:
        repo = repo_factory(model)
        db_obj = await repo.get(session, item_id)
        if db_obj is None or _is_soft_deleted(ctx, db_obj):
            raise NotFoundError(model_name, pk_name, item_id)
        if not pol.can_read(user, db_obj):
            raise ForbiddenError()
        return db_obj

    _set_param_annotations(read_resource, {"item_id": 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(
        "/{item_id}",
        response_model=read_schema,
        **_op_kwargs(ctx, Operation.UPDATE),
    )
    async def update_resource(
        item_id: PKValue,
        obj_in: BaseModel,
        session: Annotated[AsyncSession, Depends(ctx.session_dep)],
        user: Annotated[User, user_dep],
    ) -> SQLModel:
        repo = repo_factory(model)
        db_obj = await repo.get(session, item_id)
        if db_obj is None or _is_soft_deleted(ctx, db_obj):
            raise NotFoundError(model_name, pk_name, item_id)
        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

    _set_param_annotations(update_resource, {"item_id": pk_type, "obj_in": update_schema})


def add_async_delete(ctx: RouterContext) -> None:
    """Register the async DELETE endpoint."""
    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
    soft_delete = ctx.soft_delete

    @ctx.router.delete(
        "/{item_id}",
        status_code=status.HTTP_204_NO_CONTENT,
        **_op_kwargs(ctx, Operation.DELETE),
    )
    async def delete_resource(
        item_id: PKValue,
        session: Annotated[AsyncSession, Depends(ctx.session_dep)],
        user: Annotated[User, user_dep],
    ) -> None:
        repo = repo_factory(model)
        db_obj = await repo.get(session, item_id)
        if db_obj is None or _is_soft_deleted(ctx, db_obj):
            raise NotFoundError(model_name, pk_name, item_id)
        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)
        if soft_delete is not None:
            setattr(db_obj, soft_delete.field, datetime.now(timezone.utc))
            session.add(db_obj)
            await session.commit()
        else:
            await repo.delete(session, db_obj=db_obj)
        if hooks.after_delete:
            await hooks.after_delete(session, db_obj, user)

    _set_param_annotations(delete_resource, {"item_id": pk_type})


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

    if TYPE_CHECKING:
        bulk_response_model = list[BaseModel]
    else:
        bulk_response_model = list[read_schema]

    @ctx.router.post(
        "/bulk",
        response_model=bulk_response_model,
        status_code=status.HTTP_201_CREATED,
        **_op_kwargs(ctx, Operation.BULK_CREATE),
    )
    async def bulk_create(
        objects_in: list[BaseModel],
        session: Annotated[AsyncSession, Depends(ctx.session_dep)],
        user: Annotated[User, user_dep],
    ) -> list[SQLModel]:
        pol = ctx.policy
        for obj in objects_in:
            if not pol.can_create(user, obj):
                raise ForbiddenError()
        repo = repo_factory(model)
        return list(await repo.bulk_create(session, objects_in=objects_in))

    _set_param_annotations(bulk_create, {"objects_in": GenericAlias(list, create_schema)})


def _build_bulk_update_schema(ctx: RouterContext) -> type[BaseModel]:
    """Build a schema with PK + update fields for bulk update items."""
    if TYPE_CHECKING:
        return BaseModel
    pk_info = ctx.model.model_fields.get(ctx.pk_name)
    pk_annotation = pk_info.annotation if pk_info else int
    update_fields: dict[str, ModelFieldDefinition] = {ctx.pk_name: (pk_annotation, ...)}
    for name, field_info in ctx.schemas.update.model_fields.items():
        update_fields[name] = (field_info.annotation, field_info.default)
    field_definitions = cast(dict[str, ModelFieldDefinition], dict(update_fields))
    _create_model = cast(Callable[..., type[BaseModel]], create_model)
    return _create_model(
        f"{ctx.model_name}BulkUpdateItem",
        __base__=BaseModel,
        **field_definitions,
    )


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

    bulk_update_schema = _build_bulk_update_schema(ctx)

    if TYPE_CHECKING:
        bulk_update_response = list[BaseModel]
    else:
        bulk_update_response = list[read_schema]

    @ctx.router.patch(
        "/bulk",
        response_model=bulk_update_response,
        **_op_kwargs(ctx, Operation.BULK_UPDATE),
    )
    async def bulk_update(
        items_in: list[BaseModel],
        session: Annotated[AsyncSession, Depends(ctx.session_dep)],
        user: Annotated[User, user_dep],
    ) -> list[SQLModel]:
        repo = repo_factory(model)
        pairs: list[tuple[SQLModel, BaseModel]] = []
        for item in items_in:
            pk = getattr(item, pk_name)
            db_obj = await repo.get(session, pk)
            if db_obj is None or _is_soft_deleted(ctx, db_obj):
                raise NotFoundError(model_name, pk_name, pk)
            if not pol.can_update(user, db_obj, item):
                raise ForbiddenError()
            # Build an update model excluding the PK field
            update_data = item.model_dump(exclude={pk_name}, exclude_unset=True)
            update_obj = ctx.schemas.update.model_validate(update_data)
            pairs.append((db_obj, update_obj))
        return list(await repo.bulk_update(session, items=pairs))

    _set_param_annotations(
        bulk_update, {"items_in": GenericAlias(list, bulk_update_schema)}
    )


def add_async_bulk_delete(ctx: RouterContext) -> None:
    """Register the async BULK DELETE endpoint."""
    user_dep = _make_user_dep(ctx.auth, Operation.BULK_DELETE)
    model = ctx.model
    model_name = ctx.model_name
    repo_factory = ctx.repository_factory
    pol = ctx.policy
    pk_name = ctx.pk_name
    soft_delete = ctx.soft_delete

    @ctx.router.delete(
        "/bulk",
        status_code=status.HTTP_204_NO_CONTENT,
        **_op_kwargs(ctx, Operation.BULK_DELETE),
    )
    async def bulk_delete(
        ids: list[PKValue],
        session: Annotated[AsyncSession, Depends(ctx.session_dep)],
        user: Annotated[User, user_dep],
    ) -> None:
        repo = repo_factory(model)
        db_objects = []
        for pk in ids:
            db_obj = await repo.get(session, pk)
            if db_obj is None or _is_soft_deleted(ctx, db_obj):
                raise NotFoundError(model_name, pk_name, pk)
            if not pol.can_delete(user, db_obj):
                raise ForbiddenError()
            db_objects.append(db_obj)
        if soft_delete is not None:
            for db_obj in db_objects:
                setattr(db_obj, soft_delete.field, datetime.now(timezone.utc))
                session.add(db_obj)
            await session.commit()
        else:
            await repo.bulk_delete(session, db_objects=db_objects)
