from __future__ import annotations from typing import Optional from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.dependencies import get_current_user, get_db from app.crud.docker_container import DockerContainerRepository from app.crud.favorites import FavoriteContainerRepository, FavoriteGroupRepository from app.crud.host import HostRepository from app.schemas.favorites import ( FavoriteContainerCreate, FavoriteContainerOut, FavoriteContainersListResponse, FavoriteDockerContainerOut, FavoriteGroupCreate, FavoriteGroupOut, FavoriteGroupsListResponse, FavoriteGroupUpdate, ) router = APIRouter() def _resolve_user_id(user: dict) -> Optional[int]: # For API-key auth, we keep favorites in a shared (user_id NULL) namespace. if user.get("type") == "api_key": return None uid = user.get("user_id") if uid is None: return None try: return int(uid) except Exception: return None async def _favorite_to_out(db: AsyncSession, favorite, docker_container, host) -> FavoriteContainerOut: dc = FavoriteDockerContainerOut( host_id=docker_container.host_id, host_name=host.name if host else "Unknown", host_ip=host.ip_address if host else "Unknown", host_docker_status=host.docker_status if host else None, container_id=docker_container.container_id, name=docker_container.name, image=docker_container.image, state=docker_container.state, status=docker_container.status, health=docker_container.health, ports=docker_container.ports, compose_project=docker_container.compose_project, ) return FavoriteContainerOut( id=favorite.id, group_id=favorite.group_id, created_at=favorite.created_at, docker_container=dc, ) @router.get("/groups", response_model=FavoriteGroupsListResponse) async def list_favorite_groups( user: dict = Depends(get_current_user), db_session: AsyncSession = Depends(get_db), ): user_id = _resolve_user_id(user) repo = FavoriteGroupRepository(db_session) groups = await repo.list_by_user(user_id) return FavoriteGroupsListResponse(groups=[FavoriteGroupOut.model_validate(g) for g in groups]) @router.post("/groups", response_model=FavoriteGroupOut, status_code=status.HTTP_201_CREATED) async def create_favorite_group( payload: FavoriteGroupCreate, user: dict = Depends(get_current_user), db_session: AsyncSession = Depends(get_db), ): user_id = _resolve_user_id(user) repo = FavoriteGroupRepository(db_session) if await repo.exists_name(payload.name, user_id): raise HTTPException(status_code=400, detail="Un groupe avec ce nom existe déjà") group = await repo.create( user_id=user_id, name=payload.name, sort_order=payload.sort_order, color=payload.color, icon_key=payload.icon_key, ) await db_session.commit() await db_session.refresh(group) return FavoriteGroupOut.model_validate(group) @router.patch("/groups/{group_id}", response_model=FavoriteGroupOut) async def update_favorite_group( group_id: int, payload: FavoriteGroupUpdate, user: dict = Depends(get_current_user), db_session: AsyncSession = Depends(get_db), ): user_id = _resolve_user_id(user) repo = FavoriteGroupRepository(db_session) group = await repo.get_for_user(group_id, user_id) if not group: raise HTTPException(status_code=404, detail="Groupe introuvable") if payload.name and payload.name != group.name: if await repo.exists_name(payload.name, user_id): raise HTTPException(status_code=400, detail="Un groupe avec ce nom existe déjà") group = await repo.update( group, name=payload.name, sort_order=payload.sort_order, color=payload.color, icon_key=payload.icon_key, ) await db_session.commit() await db_session.refresh(group) return FavoriteGroupOut.model_validate(group) @router.delete("/groups/{group_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_favorite_group( group_id: int, user: dict = Depends(get_current_user), db_session: AsyncSession = Depends(get_db), ): user_id = _resolve_user_id(user) repo = FavoriteGroupRepository(db_session) group = await repo.get_for_user(group_id, user_id) if not group: raise HTTPException(status_code=404, detail="Groupe introuvable") await repo.delete(group) await db_session.commit() @router.get("/containers", response_model=FavoriteContainersListResponse) async def list_favorite_containers( user: dict = Depends(get_current_user), db_session: AsyncSession = Depends(get_db), ): user_id = _resolve_user_id(user) fav_repo = FavoriteContainerRepository(db_session) container_repo = DockerContainerRepository(db_session) host_repo = HostRepository(db_session) favorites = await fav_repo.list_by_user(user_id) out: list[FavoriteContainerOut] = [] for fav in favorites: docker_container = await container_repo.get(fav.docker_container_id) if not docker_container: # Should be cleaned by FK cascade, but keep the API robust. continue host = await host_repo.get(docker_container.host_id) out.append(await _favorite_to_out(db_session, fav, docker_container, host)) return FavoriteContainersListResponse(containers=out) @router.post("/containers", response_model=FavoriteContainerOut, status_code=status.HTTP_201_CREATED) async def add_favorite_container( payload: FavoriteContainerCreate, user: dict = Depends(get_current_user), db_session: AsyncSession = Depends(get_db), ): user_id = _resolve_user_id(user) container_repo = DockerContainerRepository(db_session) fav_repo = FavoriteContainerRepository(db_session) group_repo = FavoriteGroupRepository(db_session) docker_container = await container_repo.get_by_container_id(payload.host_id, payload.container_id) if not docker_container: raise HTTPException(status_code=404, detail="Container introuvable") if payload.group_id is not None: group = await group_repo.get_for_user(payload.group_id, user_id) if not group: raise HTTPException(status_code=404, detail="Groupe introuvable") existing = await fav_repo.get_by_docker_container_id(docker_container.id, user_id) if existing: # If already exists, treat as idempotent and optionally move group. if existing.group_id != payload.group_id: await fav_repo.update_group(existing, group_id=payload.group_id) await db_session.commit() await db_session.refresh(existing) host_repo = HostRepository(db_session) host = await host_repo.get(docker_container.host_id) return await _favorite_to_out(db_session, existing, docker_container, host) fav = await fav_repo.create(user_id=user_id, docker_container_db_id=docker_container.id, group_id=payload.group_id) await db_session.commit() await db_session.refresh(fav) host_repo = HostRepository(db_session) host = await host_repo.get(docker_container.host_id) return await _favorite_to_out(db_session, fav, docker_container, host) @router.delete("/containers/{favorite_id}", status_code=status.HTTP_204_NO_CONTENT) async def remove_favorite_container( favorite_id: int, user: dict = Depends(get_current_user), db_session: AsyncSession = Depends(get_db), ): user_id = _resolve_user_id(user) fav_repo = FavoriteContainerRepository(db_session) fav = await fav_repo.get_for_user(favorite_id, user_id) if not fav: raise HTTPException(status_code=404, detail="Favori introuvable") await fav_repo.delete(fav) await db_session.commit()