""" Recommendation API - Personalized suggestions for users. """ from typing import List, Optional from fastapi import APIRouter, Depends, Query from sqlmodel import Session, select, desc, func from pydantic import BaseModel from datetime import datetime, timedelta from database import get_session from models import Show, Vertical, UserVerticalPreference, Attendance, Rating, Performance, Song, Venue, BandMembership from auth import get_current_user from models import User router = APIRouter(prefix="/recommendations", tags=["recommendations"]) class RecommendedShow(BaseModel): id: int date: str venue_name: str | None vertical_name: str vertical_slug: str reason: str # "Recent Show", "Highly Rated", "Trending" class RecommendedPerformance(BaseModel): id: int song_title: str show_date: str vertical_name: str avg_rating: float notes: str | None @router.get("/shows/recent", response_model=List[RecommendedShow]) def get_recent_subscriptions( limit: int = 10, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """ Get recent shows from bands the user follows, excluding attended shows. """ # 1. Get user preferences prefs = session.exec( select(UserVerticalPreference).where(UserVerticalPreference.user_id == current_user.id) ).all() if not prefs: # Fallback: Just return recent shows from featured verticals # For now, return empty or generic return [] subscribed_vertical_ids = [p.vertical_id for p in prefs] # 2. Get Attended Show IDs attended = session.exec( select(Attendance.show_id).where(Attendance.user_id == current_user.id) ).all() attended_ids = set(attended) # 3. Query Recent Shows query = ( select(Show) .where(Show.vertical_id.in_(subscribed_vertical_ids)) .where(Show.date <= datetime.now()) # Past shows only .where(Show.date >= datetime.now() - timedelta(days=90)) # Last 90 days .order_by(desc(Show.date)) .limit(limit * 2) # Fetch extra to filter ) shows = session.exec(query).all() results = [] for show in shows: if show.id in attended_ids: continue vertical = session.get(Vertical, show.vertical_id) venue = session.get(Venue, show.venue_id) if show.venue_id else None results.append(RecommendedShow( id=show.id, date=show.date.strftime("%Y-%m-%d"), venue_name=venue.name if venue else "Unknown Venue", vertical_name=vertical.name, vertical_slug=vertical.slug, reason="Recent from your bands" )) if len(results) >= limit: break return results @router.get("/performances/top", response_model=List[RecommendedPerformance]) def get_top_rated_tracks( limit: int = 10, session: Session = Depends(get_session), current_user: User = Depends(get_current_user) ): """ Get top rated performances from bands the user follows. """ prefs = session.exec( select(UserVerticalPreference).where(UserVerticalPreference.user_id == current_user.id) ).all() if not prefs: return [] subscribed_vertical_ids = [p.vertical_id for p in prefs] # Complex query: Join Performance -> Show -> Vertical, Join Rating # Getting avg rating per performance # This might be slow on large datasets without materialized view. # Optimized approach: Query Rating table, group by performance_id, filter by subscribed verticals results = session.exec( select( Rating.performance_id, func.avg(Rating.score).label("average"), func.count(Rating.id).label("count") ) .join(Performance, Rating.performance_id == Performance.id) .join(Show, Performance.show_id == Show.id) .where(Show.vertical_id.in_(subscribed_vertical_ids)) .where(Rating.performance_id.isnot(None)) .group_by(Rating.performance_id) .having(func.count(Rating.id) >= 1) # At least 1 rating .order_by(desc("average")) .limit(limit) ).all() recommendations = [] for row in results: perf_id, avg, count = row perf = session.get(Performance, perf_id) if not perf: continue show = session.get(Show, perf.show_id) song = session.get(Song, perf.song_id) vertical = session.get(Vertical, show.vertical_id) recommendations.append(RecommendedPerformance( id=perf.id, song_title=song.title, show_date=show.date.strftime("%Y-%m-%d"), vertical_name=vertical.name, avg_rating=round(avg, 1), notes=f"Rated {round(avg, 1)}/10 by {count} fans" )) return recommendations