File size: 7,112 Bytes
05a9e8e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
"""``/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)