# app/api/v1/endpoints/chores.py import logging from typing import List as PyList, Optional from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_transactional_session from app.auth import current_active_user from app.models import User as UserModel, Chore as ChoreModel, ChoreTypeEnum from app.schemas.chore import ChoreCreate, ChoreUpdate, ChorePublic from app.crud import chore as crud_chore from app.core.exceptions import ChoreNotFoundError, PermissionDeniedError, GroupNotFoundError, DatabaseIntegrityError logger = logging.getLogger(__name__) router = APIRouter() # --- Personal Chores Endpoints --- @router.post( "/personal", response_model=ChorePublic, status_code=status.HTTP_201_CREATED, summary="Create Personal Chore", tags=["Chores", "Personal Chores"] ) async def create_personal_chore( chore_in: ChoreCreate, db: AsyncSession = Depends(get_transactional_session), current_user: UserModel = Depends(current_active_user), ): """Creates a new personal chore for the current user.""" logger.info(f"User {current_user.email} creating personal chore: {chore_in.name}") if chore_in.type != ChoreTypeEnum.personal: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Chore type must be personal.") if chore_in.group_id is not None: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="group_id must be null for personal chores.") try: return await crud_chore.create_chore(db=db, chore_in=chore_in, user_id=current_user.id) except ValueError as e: logger.warning(f"ValueError creating personal chore for user {current_user.email}: {str(e)}") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) except DatabaseIntegrityError as e: logger.error(f"DatabaseIntegrityError creating personal chore for {current_user.email}: {e.detail}", exc_info=True) raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail) @router.get( "/personal", response_model=PyList[ChorePublic], summary="List Personal Chores", tags=["Chores", "Personal Chores"] ) async def list_personal_chores( db: AsyncSession = Depends(get_transactional_session), current_user: UserModel = Depends(current_active_user), ): """Retrieves all personal chores for the current user.""" logger.info(f"User {current_user.email} listing their personal chores") return await crud_chore.get_personal_chores(db=db, user_id=current_user.id) @router.put( "/personal/{chore_id}", response_model=ChorePublic, summary="Update Personal Chore", tags=["Chores", "Personal Chores"] ) async def update_personal_chore( chore_id: int, chore_in: ChoreUpdate, db: AsyncSession = Depends(get_transactional_session), current_user: UserModel = Depends(current_active_user), ): """Updates a personal chore for the current user.""" logger.info(f"User {current_user.email} updating personal chore ID: {chore_id}") if chore_in.type is not None and chore_in.type != ChoreTypeEnum.personal: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot change chore type to group via this endpoint.") if chore_in.group_id is not None: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="group_id must be null for personal chores.") try: updated_chore = await crud_chore.update_chore(db=db, chore_id=chore_id, chore_in=chore_in, user_id=current_user.id, group_id=None) if not updated_chore: raise ChoreNotFoundError(chore_id=chore_id) if updated_chore.type != ChoreTypeEnum.personal or updated_chore.created_by_id != current_user.id: # This should ideally be caught by the CRUD layer permission checks raise PermissionDeniedError(detail="Chore is not a personal chore of the current user or does not exist.") return updated_chore except ChoreNotFoundError as e: logger.warning(f"Personal chore {e.chore_id} not found for user {current_user.email} during update.") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=e.detail) except PermissionDeniedError as e: logger.warning(f"Permission denied for user {current_user.email} updating personal chore {chore_id}: {e.detail}") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail) except ValueError as e: logger.warning(f"ValueError updating personal chore {chore_id} for user {current_user.email}: {str(e)}") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) except DatabaseIntegrityError as e: logger.error(f"DatabaseIntegrityError updating personal chore {chore_id} for {current_user.email}: {e.detail}", exc_info=True) raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail) @router.delete( "/personal/{chore_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete Personal Chore", tags=["Chores", "Personal Chores"] ) async def delete_personal_chore( chore_id: int, db: AsyncSession = Depends(get_transactional_session), current_user: UserModel = Depends(current_active_user), ): """Deletes a personal chore for the current user.""" logger.info(f"User {current_user.email} deleting personal chore ID: {chore_id}") try: # First, verify it's a personal chore belonging to the user chore_to_delete = await crud_chore.get_chore_by_id(db, chore_id) if not chore_to_delete or chore_to_delete.type != ChoreTypeEnum.personal or chore_to_delete.created_by_id != current_user.id: raise ChoreNotFoundError(chore_id=chore_id, detail="Personal chore not found or not owned by user.") success = await crud_chore.delete_chore(db=db, chore_id=chore_id, user_id=current_user.id, group_id=None) if not success: # This case should be rare if the above check passes and DB is consistent raise ChoreNotFoundError(chore_id=chore_id) return Response(status_code=status.HTTP_204_NO_CONTENT) except ChoreNotFoundError as e: logger.warning(f"Personal chore {e.chore_id} not found for user {current_user.email} during delete.") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=e.detail) except PermissionDeniedError as e: # Should be caught by the check above logger.warning(f"Permission denied for user {current_user.email} deleting personal chore {chore_id}: {e.detail}") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail) except DatabaseIntegrityError as e: logger.error(f"DatabaseIntegrityError deleting personal chore {chore_id} for {current_user.email}: {e.detail}", exc_info=True) raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail) # --- Group Chores Endpoints --- # (These would be similar to what you might have had before, but now explicitly part of this router) @router.post( "/groups/{group_id}/chores", response_model=ChorePublic, status_code=status.HTTP_201_CREATED, summary="Create Group Chore", tags=["Chores", "Group Chores"] ) async def create_group_chore( group_id: int, chore_in: ChoreCreate, db: AsyncSession = Depends(get_transactional_session), current_user: UserModel = Depends(current_active_user), ): """Creates a new chore within a specific group.""" logger.info(f"User {current_user.email} creating chore in group {group_id}: {chore_in.name}") if chore_in.type != ChoreTypeEnum.group: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Chore type must be group.") if chore_in.group_id != group_id and chore_in.group_id is not None: # Make sure chore_in.group_id matches path if provided raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Chore's group_id ({chore_in.group_id}) must match path group_id ({group_id}) or be omitted.") # Ensure chore_in has the correct group_id and type for the CRUD operation chore_payload = chore_in.model_copy(update={"group_id": group_id, "type": ChoreTypeEnum.group}) try: return await crud_chore.create_chore(db=db, chore_in=chore_payload, user_id=current_user.id, group_id=group_id) except GroupNotFoundError as e: logger.warning(f"Group {e.group_id} not found for chore creation by user {current_user.email}.") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=e.detail) except PermissionDeniedError as e: logger.warning(f"Permission denied for user {current_user.email} in group {group_id} for chore creation: {e.detail}") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail) except ValueError as e: logger.warning(f"ValueError creating group chore for user {current_user.email} in group {group_id}: {str(e)}") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) except DatabaseIntegrityError as e: logger.error(f"DatabaseIntegrityError creating group chore for {current_user.email} in group {group_id}: {e.detail}", exc_info=True) raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail) @router.get( "/groups/{group_id}/chores", response_model=PyList[ChorePublic], summary="List Group Chores", tags=["Chores", "Group Chores"] ) async def list_group_chores( group_id: int, db: AsyncSession = Depends(get_transactional_session), current_user: UserModel = Depends(current_active_user), ): """Retrieves all chores for a specific group, if the user is a member.""" logger.info(f"User {current_user.email} listing chores for group {group_id}") try: return await crud_chore.get_chores_by_group_id(db=db, group_id=group_id, user_id=current_user.id) except PermissionDeniedError as e: logger.warning(f"Permission denied for user {current_user.email} accessing chores for group {group_id}: {e.detail}") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail) @router.put( "/groups/{group_id}/chores/{chore_id}", response_model=ChorePublic, summary="Update Group Chore", tags=["Chores", "Group Chores"] ) async def update_group_chore( group_id: int, chore_id: int, chore_in: ChoreUpdate, db: AsyncSession = Depends(get_transactional_session), current_user: UserModel = Depends(current_active_user), ): """Updates a chore's details within a specific group.""" logger.info(f"User {current_user.email} updating chore ID {chore_id} in group {group_id}") if chore_in.type is not None and chore_in.type != ChoreTypeEnum.group: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot change chore type to personal via this endpoint.") if chore_in.group_id is not None and chore_in.group_id != group_id: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Chore's group_id if provided must match path group_id ({group_id}).") # Ensure chore_in has the correct type for the CRUD operation chore_payload = chore_in.model_copy(update={"type": ChoreTypeEnum.group, "group_id": group_id} if chore_in.type is None else chore_in) try: updated_chore = await crud_chore.update_chore(db=db, chore_id=chore_id, chore_in=chore_payload, user_id=current_user.id, group_id=group_id) if not updated_chore: raise ChoreNotFoundError(chore_id=chore_id, group_id=group_id) return updated_chore except ChoreNotFoundError as e: logger.warning(f"Chore {e.chore_id} in group {e.group_id} not found for user {current_user.email} during update.") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=e.detail) except PermissionDeniedError as e: logger.warning(f"Permission denied for user {current_user.email} updating chore {chore_id} in group {group_id}: {e.detail}") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail) except ValueError as e: logger.warning(f"ValueError updating group chore {chore_id} for user {current_user.email} in group {group_id}: {str(e)}") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) except DatabaseIntegrityError as e: logger.error(f"DatabaseIntegrityError updating group chore {chore_id} for {current_user.email}: {e.detail}", exc_info=True) raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail) @router.delete( "/groups/{group_id}/chores/{chore_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete Group Chore", tags=["Chores", "Group Chores"] ) async def delete_group_chore( group_id: int, chore_id: int, db: AsyncSession = Depends(get_transactional_session), current_user: UserModel = Depends(current_active_user), ): """Deletes a chore from a group, ensuring user has permission.""" logger.info(f"User {current_user.email} deleting chore ID {chore_id} from group {group_id}") try: # Verify chore exists and belongs to the group before attempting deletion via CRUD # This gives a more precise error if the chore exists but isn't in this group. chore_to_delete = await crud_chore.get_chore_by_id_and_group(db, chore_id, group_id, current_user.id) # checks permission too if not chore_to_delete : # get_chore_by_id_and_group will raise PermissionDeniedError if user not member raise ChoreNotFoundError(chore_id=chore_id, group_id=group_id) success = await crud_chore.delete_chore(db=db, chore_id=chore_id, user_id=current_user.id, group_id=group_id) if not success: # This case should be rare if the above check passes and DB is consistent raise ChoreNotFoundError(chore_id=chore_id, group_id=group_id) return Response(status_code=status.HTTP_204_NO_CONTENT) except ChoreNotFoundError as e: logger.warning(f"Chore {e.chore_id} in group {e.group_id} not found for user {current_user.email} during delete.") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=e.detail) except PermissionDeniedError as e: logger.warning(f"Permission denied for user {current_user.email} deleting chore {chore_id} in group {group_id}: {e.detail}") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail) except DatabaseIntegrityError as e: logger.error(f"DatabaseIntegrityError deleting group chore {chore_id} for {current_user.email}: {e.detail}", exc_info=True) raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail)