"""Nested endpoint builders."""

from typing import Annotated, cast

from fastapi import Depends, status
from pydantic import BaseModel
from sqlalchemy.exc import IntegrityError
from sqlmodel import SQLModel
from sqlmodel.ext.asyncio.session import AsyncSession

from auen._async_endpoints_common import (
    _build_nested_create_schema,
    _build_nested_update_schema,
    _op_kwargs,
    _rename_path_param,
    _set_param_annotations,
)
from auen._endpoints import RouterContext, _make_user_dep
from auen._nested_mutations import apply_nested_creates, apply_nested_mutations
from auen.config import Operation
from auen.exceptions import ConflictError, ForbiddenError, NotFoundError
from auen.types import PKValue, User


def add_async_nested_create(ctx: RouterContext) -> None:
    """Register the async NESTED CREATE endpoint."""
    create_schema = _build_nested_create_schema(ctx)
    read_schema = ctx.schemas.read
    user_dep = _make_user_dep(ctx.auth, Operation.NESTED_CREATE)
    model = ctx.model
    pol = ctx.policy

    @ctx.router.post(
        "/nested",
        response_model=read_schema,
        status_code=status.HTTP_201_CREATED,
        **_op_kwargs(ctx, Operation.NESTED_CREATE),
    )
    async def nested_create_resource(
        obj_in: BaseModel,
        session: Annotated[AsyncSession, Depends(ctx.session_dep)],
        user: Annotated[User, user_dep],
    ) -> SQLModel:
        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)

        create_data = obj_in.model_dump(exclude_unset=True)
        db_obj = await apply_nested_creates(
            session=session,
            user=user,
            model=model,
            create_data=cast(dict[str, object], create_data),
            nested_writes=ctx.nested_writes,
            nested_policy_registry=ctx.nested_policy_registry,
        )
        try:
            await session.commit()
        except IntegrityError as e:
            await session.rollback()
            raise ConflictError(detail="Integrity constraint violation") from e
        await session.refresh(db_obj)

        if hooks.after_create:
            await hooks.after_create(session, db_obj, user)
        return db_obj

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


def add_async_nested_update(ctx: RouterContext) -> None:
    """Register the async NESTED UPDATE endpoint."""
    update_schema = _build_nested_update_schema(ctx)
    read_schema = ctx.schemas.read
    user_dep = _make_user_dep(ctx.auth, Operation.NESTED_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 + "}/nested",
        response_model=read_schema,
        **_op_kwargs(ctx, Operation.NESTED_UPDATE),
    )
    async def nested_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)

        update_data = obj_in.model_dump(exclude_unset=True)
        await apply_nested_mutations(
            session=session,
            user=user,
            model=model,
            db_obj=db_obj,
            update_data=cast(dict[str, object], update_data),
            nested_writes=ctx.nested_writes,
            nested_policy_registry=ctx.nested_policy_registry,
        )

        update_obj = ctx.schemas.update.model_validate(update_data)
        result = await repo.update(session, db_obj=db_obj, obj_in=update_obj)
        if hooks.after_update:
            await hooks.after_update(session, result, user)
        return result

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