import json from datetime import datetime, timezone from fastapi import APIRouter, HTTPException, Request, Query from pydantic import BaseModel from typing import Optional from database import fetch_one, fetch_all, execute from auth import get_current_user from config import TMDB_IMAGE_BASE, DEV_MODE from ratelimit import limiter router = APIRouter() async def _resolve_user(request: Request) -> dict: user_data = await get_current_user(request) user = await fetch_one("SELECT * FROM users WHERE user_id = ?", [str(user_data["id"])]) if not user: raise HTTPException(status_code=404, detail="User not found") return user class ProfileUpdate(BaseModel): first_name: Optional[str] = None last_name: Optional[str] = None username: Optional[str] = None photo_url: Optional[str] = None class RatingRequest(BaseModel): tmdb_id: int media_type: str rating: int review: Optional[str] = None class AssetActivate(BaseModel): asset_item_id: str product_id: str class ListAdd(BaseModel): tmdb_id: int media_type: str title: str poster_url: Optional[str] = None class ListRemove(BaseModel): tmdb_id: int media_type: str @router.get("/me") async def get_my_profile(request: Request): user = await _resolve_user(request) watch_count = await fetch_one("SELECT COUNT(*) as cnt FROM watch_history WHERE user_id = ?", [str(user["id"])]) series_completed = await fetch_one( "SELECT COUNT(DISTINCT tmdb_id) as cnt FROM watch_history WHERE user_id = ? AND media_type='tv' AND completed=1", [str(user["id"])], ) friends_count = await fetch_one( "SELECT COUNT(*) as cnt FROM friendships WHERE (user_id = ? OR friend_id = ?) AND status='accepted'", [str(user["user_id"]), str(user["user_id"])], ) return { "ok": True, "data": { "id": user["id"], "user_id": user["user_id"], "username": user.get("username", ""), "first_name": user.get("first_name", ""), "last_name": user.get("last_name", ""), "photo_url": user.get("photo_url", ""), "language_code": user.get("language_code", "en"), "is_premium": bool(user.get("is_premium")), "stars_balance": user.get("stars_balance", 0), "kernels_balance": user.get("kernels_balance", 0), "total_watch_time": user.get("total_watch_time", 0), "last_active_at": user.get("last_active_at", ""), "watch_count": watch_count["cnt"] if watch_count else 0, "series_completed": series_completed["cnt"] if series_completed else 0, "friends_count": friends_count["cnt"] if friends_count else 0, }, } @router.patch("/me") async def update_profile(body: ProfileUpdate, request: Request): user = await _resolve_user(request) updates = {} if body.first_name is not None: updates["first_name"] = body.first_name if body.last_name is not None: updates["last_name"] = body.last_name if body.username is not None: updates["username"] = body.username if body.photo_url is not None: updates["photo_url"] = body.photo_url if updates: set_clause = ", ".join(f"{k}=?" for k in updates) vals = [str(v) for v in updates.values()] + [str(user["user_id"])] await execute(f"UPDATE users SET {set_clause} WHERE user_id=?", vals) return {"ok": True, "data": {"updated": list(updates.keys())}} # Ratings @router.post("/rating") async def create_rating(body: RatingRequest, request: Request): user = await _resolve_user(request) if body.rating < 1 or body.rating > 10: raise HTTPException(status_code=400, detail="Rating must be 1-10") await execute( """INSERT INTO ratings (user_id, tmdb_id, media_type, rating, review, updated_at) VALUES (?, ?, ?, ?, ?, datetime('now')) ON CONFLICT(user_id, tmdb_id, media_type) DO UPDATE SET rating=?, review=?, updated_at=datetime('now')""", [ str(user["id"]), str(body.tmdb_id), body.media_type, str(body.rating), body.review or "", str(body.rating), body.review or "", ], ) return {"ok": True, "data": {"saved": True}} @router.get("/me/ratings") async def get_my_ratings(request: Request): user = await _resolve_user(request) rows = await fetch_all( "SELECT * FROM ratings WHERE user_id = ? ORDER BY updated_at DESC", [str(user["id"])] ) return {"ok": True, "data": rows} # Assets @router.get("/me/assets") async def get_my_assets(request: Request): user = await _resolve_user(request) rows = await fetch_all( """SELECT ua.*, ai.name_en, ai.image_url, ai.rarity, ai.type as asset_type_name FROM user_assets ua LEFT JOIN asset_items ai ON ua.asset_id = ai.id WHERE ua.user_id = ? ORDER BY ua.acquired_at DESC""", [str(user["id"])], ) return {"ok": True, "data": rows} @router.post("/me/asset/activate") async def activate_asset(body: AssetActivate, request: Request): user = await _resolve_user(request) await execute( "UPDATE user_assets SET is_active=0 WHERE user_id=?", [str(user["id"])], ) await execute( "UPDATE user_assets SET is_active=1 WHERE user_id=? AND asset_item_id=? AND product_id=?", [str(user["id"]), body.asset_item_id, body.product_id], ) return {"ok": True, "data": {"activated": True}} # Wallet @router.get("/me/wallet") async def get_wallet(request: Request): user = await _resolve_user(request) return { "ok": True, "data": { "stars": user.get("stars_balance", 0), "kernels": user.get("kernels_balance", 0), "points": user.get("points", 0), }, } @router.get("/me/wallet/kernels") async def get_kernels(request: Request): user = await _resolve_user(request) return { "ok": True, "data": { "balance": user.get("kernels_balance", 0), "total_earned": user.get("total_earned_kernels", 0), "total_spent": user.get("total_spent_kernels", 0), }, } # Daily Streak @router.get("/me/daily-streak") async def get_daily_streak(request: Request): user = await _resolve_user(request) streak = await fetch_one("SELECT * FROM daily_streaks WHERE user_id = ?", [str(user["id"])]) if not streak: return {"ok": True, "data": {"streak": 0, "can_claim": True, "days": []}} last_claim = streak.get("last_claim_date", "") today = datetime.now(timezone.utc).strftime("%Y-%m-%d") can_claim = last_claim != today return { "ok": True, "data": { "streak": streak.get("streak_count", 0), "last_claim": last_claim or "", "can_claim": can_claim or DEV_MODE, "today_claimed": not can_claim, "reward_today": 10, "next_reward": 10, }, } @router.post("/me/daily-streak/claim") @limiter.limit("5/minute") async def claim_daily_streak(request: Request): user = await _resolve_user(request) today = datetime.now(timezone.utc).strftime("%Y-%m-%d") streak = await fetch_one("SELECT * FROM daily_streaks WHERE user_id = ?", [str(user["id"])]) if streak: last_claim = streak.get("last_claim_date", "") if last_claim == today and not DEV_MODE: raise HTTPException(status_code=400, detail="Already claimed today") yesterday = datetime.now(timezone.utc).timestamp() - 86400 yesterday_str = datetime.fromtimestamp(yesterday, tz=timezone.utc).strftime("%Y-%m-%d") if last_claim == yesterday_str or not last_claim: new_count = (streak.get("streak_count", 0) or 0) + 1 else: new_count = 1 await execute( "UPDATE daily_streaks SET streak_count=?, last_claim_date=? WHERE user_id=?", [str(new_count), today, str(user["id"])], ) else: new_count = 1 await execute( "INSERT INTO daily_streaks (user_id, streak_count, last_claim_date) VALUES (?, 1, ?)", [str(user["id"]), today], ) await execute("UPDATE users SET stars_balance = stars_balance + 10 WHERE id = ?", [str(user["id"])]) return {"ok": True, "data": {"claimed": True, "streak": new_count, "reward": 10}} # Earnings @router.get("/me/earnings") async def get_earnings(request: Request): user = await _resolve_user(request) return {"ok": True, "data": {"kernels": user.get("kernels_balance", 0), "history": []}} # Watch List @router.get("/me/list") async def get_my_list(request: Request): user = await _resolve_user(request) rows = await fetch_all( "SELECT * FROM user_list WHERE user_id = ? ORDER BY added_at DESC", [str(user["id"])] ) return {"ok": True, "data": rows} @router.post("/me/list") async def add_to_list(body: ListAdd, request: Request): user = await _resolve_user(request) await execute( """INSERT OR IGNORE INTO user_list (user_id, tmdb_id, media_type, title, poster_url) VALUES (?, ?, ?, ?, ?)""", [str(user["id"]), str(body.tmdb_id), body.media_type, body.title, body.poster_url or ""], ) return {"ok": True, "data": {"added": True}} @router.delete("/me/list") async def remove_from_list(body: ListRemove, request: Request): user = await _resolve_user(request) await execute( "DELETE FROM user_list WHERE user_id=? AND tmdb_id=? AND media_type=?", [str(user["id"]), str(body.tmdb_id), body.media_type], ) return {"ok": True, "data": {"removed": True}} # Watch History @router.get("/me/history") async def get_watch_history(request: Request, page: int = Query(1, ge=1)): user = await _resolve_user(request) offset = (page - 1) * 20 rows = await fetch_all( f"""SELECT wh.*, a.title, a.poster_path, a.quality, a.year FROM watch_history wh LEFT JOIN (SELECT DISTINCT media_id, title, poster_path, quality, year FROM archives) a ON wh.tmdb_id = a.media_id WHERE wh.user_id = ? ORDER BY wh.last_watched_at DESC LIMIT 20 OFFSET {offset}""", [str(user["id"])], ) for r in rows: pp = r.get("poster_path", "") if pp and not pp.startswith("http"): r["poster_url"] = f"{TMDB_IMAGE_BASE}/w500{pp}" if pp.startswith("/") else pp else: r["poster_url"] = pp or "" return {"ok": True, "data": rows} # Purchases @router.get("/me/purchases") async def get_purchases(request: Request): user = await _resolve_user(request) rows = await fetch_all( """SELECT p.*, pr.name_en, pr.name_ar, pr.image_url, pr.category FROM purchases p LEFT JOIN products pr ON p.product_id = pr.id WHERE p.user_id = ? ORDER BY p.purchased_at DESC""", [str(user["id"])], ) return {"ok": True, "data": rows} # User profile (other users) @router.get("/user/{user_id}") async def get_user_profile(user_id: int): user = await fetch_one("SELECT * FROM users WHERE user_id = ?", [str(user_id)]) if not user: raise HTTPException(status_code=404, detail="User not found") watch_stats = await fetch_one( "SELECT COUNT(*) as watch_count, COALESCE(SUM(duration_seconds), 0) as total_watch_time FROM watch_history WHERE user_id=?", [str(user["id"])], ) total_watch_time = watch_stats["total_watch_time"] if watch_stats else 0 watch_count = watch_stats["watch_count"] if watch_stats else 0 return { "ok": True, "data": { "user_id": user["user_id"], "username": user.get("username", ""), "first_name": user.get("first_name", ""), "last_name": user.get("last_name", ""), "photo_url": user.get("photo_url", ""), "is_premium": bool(user.get("is_premium")), "total_watch_time": total_watch_time, "points": user.get("points", 0), "stats": { "total_watch_time": total_watch_time, "watch_count": watch_count, "points": user.get("points", 0), "kernels_balance": user.get("kernels_balance", 0), }, }, }