"""``/api/ai/recommend`` — AI-powered next-challenge recommendation. Analyzes the user's completion history and picks the challenge type they've done least, then selects a random challenge from that pool. Returns the full set of parameters the front-end needs to start a ``TrainingSession``. """ import random from typing import Optional import httpx from fastapi import APIRouter, Depends from pydantic import BaseModel from app.core.auth import get_current_user from app.core.config import SUPABASE_URL, SUPABASE_ANON_KEY from app.services.supabase_service import supabase_headers router = APIRouter() class AiRecommendResponse(BaseModel): categoryId: str pathId: str moduleId: str teamRole: str challengeId: str title: str reason: str # short AI-style justification # All five challenge types with their table, front-end params, and # the category key used in ``user_completions``. _CHALLENGE_TYPES: list[dict] = [ { "table": "encryption_challenges", "team": "red", "categoryId": "encryption", "pathId": "cryptography", "moduleId": "crypto", "completion_category": "crypto", }, { "table": "code_fixing_challenges", "team": "blue", "categoryId": "code_analysis", "pathId": "web-security", "moduleId": "code-fixing", "completion_category": "code-fixing", }, { "table": "log_analysis_challenges", "team": "blue", "categoryId": "log_analysis", "pathId": "log-analysis", "moduleId": "log-analysis", "completion_category": "log-analysis", }, { "table": "vulnerability_hunter_challenges", "team": "blue", "categoryId": "vulnerability_hunter", "pathId": "secure-coding", "moduleId": "vulnerability-hunter", "completion_category": "vulnerability-hunter", }, { "table": "web_exploitation_challenges", "team": "red", "categoryId": "web_exploitation", "pathId": "xss", "moduleId": "web-exploitation", "completion_category": "web-exploitation", }, ] # Short AI-sounding reasons keyed by completion_category _REASONS: dict[str, list[str]] = { "crypto": [ "نقاط ضعفك في التشفير تحتاج تقوية — جرّب هذا التحدي لتصقل مهاراتك", "التشفير هو خط الدفاع الأول في الأمن السيبراني — حان وقت التمرّن", "مهاراتك في فك التشفير بحاجة إلى صقل — هذا التحدي سيساعدك", ], "code-fixing": [ "اكتشاف الثغرات في الكود مهارة لا غنى عنها — طوّرها بهذا التحدي", "المبرمجون المحترفون يقرؤون الكود بحثاً عن الثغرات — كن منهم", "هذا التحدي سيدربك على تحديد نقاط الضعف في التطبيقات", ], "log-analysis": [ "تحليل السجلات يكشف الهجمات قبل فوات الأوان — درّب عينك", "محللو SOC المتميزون يميّزون الأنماط في السجلات — تدرب هنا", "هذا التحدي يحاكي هجوماً حقيقياً — حلّله وكن جاهزاً", ], "vulnerability-hunter": [ "صائد الثغرات الحقيقي لا يفوته أي خلل — اختبر نفسك", "كلما اكتشفت ثغرات أكثر، كلما أصبحت أقوى — هذا هو تحديك التالي", "ثغرات اليوم هي ثغرات الغد — تدرب على اكتشافها الآن", ], "web-exploitation": [ "الهجمات الإلكترونية تستهدف الويب أولاً — كن مستعداً", "هذا التحدي يحاكي هجوماً حقيقياً على تطبيقات الويب", "الـ Bug Bounty يبدأ بفهم الثغرات — هذا هو تحديك التالي", ], } @router.get("/api/ai/recommend") async def get_ai_recommendation(user: dict = Depends(get_current_user)): if not SUPABASE_URL or not SUPABASE_ANON_KEY: return {"error": "AI recommendation unavailable"} # 1. Get completions per category for this user completions = await _fetch_user_completions(user["user_id"]) if completions is None: completions = {} # 2. Sort challenge types by completion count (ascending) scored = [] for ct in _CHALLENGE_TYPES: cat = ct["completion_category"] count = completions.get(cat, 0) scored.append((count, ct)) scored.sort(key=lambda x: x[0]) # 3. Try each type from least-completed to most, pick one with available challenges for count, ct in scored: row = await _fetch_random_challenge(ct["table"], ct["team"]) if row is None: continue reason = _pick_reason(ct["completion_category"]) return AiRecommendResponse( categoryId=ct["categoryId"], pathId=ct["pathId"], moduleId=ct["moduleId"], teamRole=ct["team"], challengeId=row["id"], title=row.get("title", "Untitled Challenge"), reason=reason, ) return {"error": "لا توجد تحديات متاحة حالياً"} async def _fetch_user_completions(user_id: str) -> Optional[dict[str, int]]: """Return {category: count} for this user, or None on error.""" url = f"{SUPABASE_URL}/rest/v1/user_completions?user_id=eq.{user_id}&select=category" try: async with httpx.AsyncClient(timeout=10) as client: r = await client.get(url, headers=supabase_headers()) if r.status_code == 200: counts: dict[str, int] = {} for row in r.json(): cat = row.get("category", "") if cat: counts[cat] = counts.get(cat, 0) + 1 return counts except Exception as e: print(f"[ai_recommend] fetch completions error: {e}") return None async def _fetch_random_challenge(table: str, team_role: str) -> Optional[dict]: """Pick a random challenge from the given pool table.""" url = ( f"{SUPABASE_URL}/rest/v1/{table}" f"?team_role=eq.{team_role}&select=id,title" f"&limit=20&order=created_at.desc" ) try: async with httpx.AsyncClient(timeout=10) as client: r = await client.get(url, headers=supabase_headers()) if r.status_code == 200: rows = r.json() if rows: return random.choice(rows) except Exception as e: print(f"[ai_recommend] {table}: {e}") return None def _pick_reason(completion_category: str) -> str: reasons = _REASONS.get(completion_category) if not reasons: return "التحدي الأمثل لمستواك الحالي — جرّبه الآن" return random.choice(reasons)