"""Movie endpoints (section 17).""" from __future__ import annotations from typing import Literal from fastapi import APIRouter, Depends, HTTPException, Query, status from redis.asyncio import Redis from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import cached_response, get_current_user, not_found from app.core.database import get_db from app.core.redis import get_redis from app.core.security import enforce_rate_limit from app.models.user import User from app.schemas.account import RatingIn, RatingOut from app.schemas.movie import ( MovieCreditsOut, MovieDetailOut, MovieImagesOut, MovieListOut, MovieRecommendationsOut, MovieVideosOut, MovieWatchProvidersOut, ) from app.services import movie_service from app.services.cache import build_key router = APIRouter(prefix="/movies", tags=["movies"], dependencies=[Depends(enforce_rate_limit)]) @router.get("", response_model=MovieListOut) async def list_movies( genre: str | None = Query(None, description="Aurora genre id, e.g. aur_genre_..."), keyword: str | None = Query(None, description="Aurora keyword id, e.g. aur_keyword_..."), year: int | None = Query(None, description="Primary release year"), status_: str | None = Query(None, alias="status", description="Movie status, e.g. released"), sort_by: Literal["popularity", "release_date", "vote_average", "title"] = Query("popularity"), sort_dir: Literal["asc", "desc"] = Query("desc"), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=50), session: AsyncSession = Depends(get_db), ) -> MovieListOut: try: return await movie_service.list_movies( session, genre_aurora_id=genre, keyword_aurora_id=keyword, year=year, status=status_, sort_by=sort_by, sort_dir=sort_dir, page=page, page_size=page_size, ) except movie_service.InvalidMovieFilterError as exc: raise HTTPException(status.HTTP_422_UNPROCESSABLE_CONTENT, str(exc)) from exc @router.get("/now-playing", response_model=MovieListOut) async def now_playing_movies( region: str = Query("US", min_length=2, max_length=2, description="ISO 3166-1 alpha-2 country"), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=50), session: AsyncSession = Depends(get_db), ) -> MovieListOut: return await movie_service.list_now_playing(session, region=region, page=page, page_size=page_size) @router.get("/upcoming", response_model=MovieListOut) async def upcoming_movies( region: str = Query("US", min_length=2, max_length=2, description="ISO 3166-1 alpha-2 country"), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=50), session: AsyncSession = Depends(get_db), ) -> MovieListOut: return await movie_service.list_upcoming(session, region=region, page=page, page_size=page_size) @router.get("/trending", response_model=MovieListOut) async def trending_movies( time_window: Literal["day", "week"] = Query("day", alias="window"), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=50), session: AsyncSession = Depends(get_db), ) -> MovieListOut: try: return await movie_service.list_trending( session, time_window=time_window, page=page, page_size=page_size ) except movie_service.InvalidMovieFilterError as exc: raise HTTPException(status.HTTP_422_UNPROCESSABLE_CONTENT, str(exc)) from exc @router.get("/{aurora_id}", response_model=MovieDetailOut) async def get_movie( aurora_id: str, session: AsyncSession = Depends(get_db), redis: Redis = Depends(get_redis), ) -> MovieDetailOut: try: return await cached_response( redis, build_key("movie", aurora_id), MovieDetailOut, lambda: movie_service.get_movie_detail(session, aurora_id), ) except movie_service.MovieNotFoundError as exc: raise not_found("movie", aurora_id) from exc @router.get("/{aurora_id}/credits", response_model=MovieCreditsOut) async def get_movie_credits( aurora_id: str, session: AsyncSession = Depends(get_db) ) -> MovieCreditsOut: try: return await movie_service.get_movie_credits(session, aurora_id) except movie_service.MovieNotFoundError as exc: raise not_found("movie", aurora_id) from exc @router.get("/{aurora_id}/images", response_model=MovieImagesOut) async def get_movie_images( aurora_id: str, session: AsyncSession = Depends(get_db) ) -> MovieImagesOut: try: return await movie_service.get_movie_images(session, aurora_id) except movie_service.MovieNotFoundError as exc: raise not_found("movie", aurora_id) from exc @router.get("/{aurora_id}/videos", response_model=MovieVideosOut) async def get_movie_videos( aurora_id: str, session: AsyncSession = Depends(get_db) ) -> MovieVideosOut: try: return await movie_service.get_movie_videos(session, aurora_id) except movie_service.MovieNotFoundError as exc: raise not_found("movie", aurora_id) from exc @router.get("/{aurora_id}/recommendations", response_model=MovieRecommendationsOut) async def get_movie_recommendations( aurora_id: str, session: AsyncSession = Depends(get_db) ) -> MovieRecommendationsOut: try: return await movie_service.get_movie_recommendations(session, aurora_id) except movie_service.MovieNotFoundError as exc: raise not_found("movie", aurora_id) from exc @router.get("/{aurora_id}/watch-providers", response_model=MovieWatchProvidersOut) async def get_movie_watch_providers( aurora_id: str, session: AsyncSession = Depends(get_db) ) -> MovieWatchProvidersOut: try: return await movie_service.get_movie_watch_providers(session, aurora_id) except movie_service.MovieNotFoundError as exc: raise not_found("movie", aurora_id) from exc @router.put("/{aurora_id}/rating", response_model=RatingOut) async def rate_movie( aurora_id: str, body: RatingIn, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_db), ) -> RatingOut: try: return await movie_service.rate_movie(session, aurora_id, user_id=user.id, value=body.value) except movie_service.MovieNotFoundError as exc: raise not_found("movie", aurora_id) from exc @router.delete("/{aurora_id}/rating", status_code=status.HTTP_204_NO_CONTENT) async def unrate_movie( aurora_id: str, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_db), ) -> None: try: await movie_service.unrate_movie(session, aurora_id, user_id=user.id) except movie_service.MovieNotFoundError as exc: raise not_found("movie", aurora_id) from exc @router.post("/{aurora_id}/watchlist", status_code=status.HTTP_204_NO_CONTENT) async def add_movie_to_watchlist( aurora_id: str, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_db), ) -> None: try: await movie_service.add_movie_to_watchlist(session, aurora_id, user_id=user.id) except movie_service.MovieNotFoundError as exc: raise not_found("movie", aurora_id) from exc @router.delete("/{aurora_id}/watchlist", status_code=status.HTTP_204_NO_CONTENT) async def remove_movie_from_watchlist( aurora_id: str, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_db), ) -> None: try: await movie_service.remove_movie_from_watchlist(session, aurora_id, user_id=user.id) except movie_service.MovieNotFoundError as exc: raise not_found("movie", aurora_id) from exc