File size: 9,469 Bytes
91990f9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
from fastapi import APIRouter, HTTPException, Request, Depends
from config.database import get_supabase_admin
from middleware.auth_guard import get_current_user, get_current_user_optional
from models.challenge import ChallengeSubmit
from services.ai_router import route_analysis
from services.code_runner import run_code

router = APIRouter(prefix="/api/challenges", tags=["challenges"])


@router.get("/all")
async def get_challenges(difficulty: str = "", category: str = "", company: str = ""):
    db = get_supabase_admin()
    query = db.table("challenges").select("*").eq("status", "published")
    if difficulty:
        query = query.eq("difficulty", difficulty)
    if category:
        query = query.eq("category", category)
    if company:
        query = query.eq("company_tag", company)
    result = query.order("created_at", desc=True).execute()
    return result.data or []


@router.get("/interview")
async def get_interview_questions(company: str = ""):
    db = get_supabase_admin()
    query = db.table("challenges").select("*").eq("status", "published").eq("is_interview_question", True)
    if company:
        query = query.eq("company_tag", company)
    result = query.execute()
    return result.data or []


@router.get("/{challenge_id}")
async def get_challenge(challenge_id: str):
    db = get_supabase_admin()
    result = db.table("challenges").select("*").eq("id", challenge_id).eq("status", "published").single().execute()
    if not result.data:
        raise HTTPException(status_code=404, detail="Challenge not found")
    return result.data


@router.post("/{challenge_id}/submit")
async def submit_challenge(challenge_id: str, data: ChallengeSubmit, request: Request):
    user = await get_current_user_optional(request)
    db = get_supabase_admin()

    # Get challenge
    ch = db.table("challenges").select("*").eq("id", challenge_id).single().execute()
    if not ch.data:
        raise HTTPException(status_code=404, detail="Challenge not found")
    challenge = ch.data

    # Scenario 1: MCQ Challenge (Quiz or Single)
    if challenge.get('type') == 'mcq':
        questions = challenge.get('questions', [])
        
        # Multi-question Quiz Logic
        if questions and len(questions) > 0:
            if data.selected_options is None or len(data.selected_options) != len(questions):
                raise HTTPException(status_code=400, detail=f"selected_options array is required and must match questions length ({len(questions)})")
            
            results = []
            correct_count = 0
            for i, q in enumerate(questions):
                sel = data.selected_options[i]
                # Flexible key for backward compatibility or different bulk upload formats
                cor = q.get('correct_index')
                if cor is None:
                    cor = q.get('correct_option', 0)
                
                is_correct = sel == cor
                if is_correct:
                    correct_count += 1
                
                results.append({
                    "question_index": i,
                    "selected": sel,
                    "correct": cor,
                    "passed": is_correct,
                    "explanation": q.get('explanation', "No explanation provided.")
                })
            
            score = round((correct_count / len(questions)) * 100, 2)
            passed = correct_count == len(questions)
            
            ai_feedback = {
                "feedback": f"You scored {correct_count}/{len(questions)} ({score}%). " + 
                           ("Perfect! You've mastered this topic." if passed else "Good effort! Review the explanations below to improve."),
                "quiz_results": results,
                "score": score,
                "time_complexity": "N/A",
                "space_complexity": "N/A",
            }
            
            if user:
                try:
                    # Final sanity check on fields
                    insert_data = {
                        "user_id": str(user.id),
                        "challenge_id": challenge_id,
                        "challenge_type": "mcq",
                        "passed": passed,
                        "ai_feedback": ai_feedback or {},
                        "metadata": {
                            "score": score, 
                            "selected_options": data.selected_options,
                            "quiz_length": len(questions)
                        }
                    }
                    db.table("challenge_submissions").insert(insert_data).execute()
                except Exception as db_err:
                    import logging
                    logger = logging.getLogger(__name__)
                    logger.error(f"Failed to save MCQ submission: {str(db_err)}")

            return {
                "passed": passed,
                "score": score,
                "ai_feedback": ai_feedback,
            }

        # Single Question MCQ Logic (Backward compatibility)
        if data.selected_option is None:
            raise HTTPException(status_code=400, detail="selected_option is required for single-question MCQ")
        
        correct_idx = challenge.get('correct_option', 0)
        passed = data.selected_option == correct_idx
        
        ai_feedback = {
            "feedback": "Correct! You have a good understanding of this concept." if passed else "Incorrect. Review the concept and try again.",
            "correct_approach": f"The correct answer is option index {correct_idx}.",
            "time_complexity": "N/A",
            "space_complexity": "N/A",
            "improvements": []
        }
        
        if user:
            try:
                db.table("challenge_submissions").insert({
                    "user_id": str(user.id),
                    "challenge_id": challenge_id,
                    "challenge_type": "mcq",
                    "selected_option": data.selected_option,
                    "passed": passed,
                    "ai_feedback": ai_feedback,
                    "metadata": {"score": 100 if passed else 0}
                }).execute()
            except Exception as db_err:
                import logging
                logger = logging.getLogger(__name__)
                logger.error(f"Failed to save Single MCQ submission: {str(db_err)}")

        return {
            "passed": passed,
            "correct_option": correct_idx,
            "ai_feedback": ai_feedback,
        }

    # Scenario 2: Coding Challenge
    if not data.code:
        raise HTTPException(status_code=400, detail="code is required for coding challenges")

    try:
        output = await run_code(data.language, data.code)
    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))

    # Check test cases
    test_cases = challenge.get("test_cases", [])
    passed = True
    test_results = []
    for tc in test_cases:
        expected = str(tc.get("output", "")).strip()
        actual = output.strip()
        ok = expected in actual
        test_results.append({"input": tc.get("input"), "expected": expected, "actual": actual, "passed": ok})
        if not ok:
            passed = False

    # Get AI feedback
    from services.settings import is_ai_enabled
    if is_ai_enabled():
        prompt = f"""A user submitted this {data.language} code for a coding challenge.
Challenge: {challenge.get('title')}
Description: {challenge.get('description')}
Code:
{data.code}
Output: {output}
Tests passed: {passed}

Respond in JSON only:
{{
  "feedback": "2-3 sentence explanation of their solution quality",
  "correct_approach": "brief explanation of the optimal approach",
  "time_complexity": "O(?)",
  "space_complexity": "O(?)",
  "improvements": ["improvement 1", "improvement 2"]
}}"""

        try:
            ai_feedback = await route_analysis(prompt)
        except Exception:
            ai_feedback = {"feedback": "Good attempt! Keep practicing.", "correct_approach": "", "improvements": []}
    else:
        ai_feedback = {
            "feedback": "Submission processed. Detailed AI analysis is currently disabled by administrator.",
            "correct_approach": "AI analysis is required to generate the optimal approach.",
            "time_complexity": "N/A",
            "space_complexity": "N/A",
            "improvements": []
        }

    # Save submission if logged in
    if user:
        try:
            db.table("challenge_submissions").insert({
                "user_id": str(user.id),
                "challenge_id": challenge_id,
                "challenge_type": "coding",
                "code": data.code,
                "language": data.language,
                "passed": passed,
                "ai_feedback": ai_feedback,
            }).execute()
        except Exception:
            pass

    return {
        "passed": passed,
        "output": output,
        "test_results": test_results,
        "ai_feedback": ai_feedback,
    }


@router.get("/{challenge_id}/submissions")
async def get_my_submissions(challenge_id: str, request: Request):
    user = await get_current_user(request)
    db = get_supabase_admin()
    result = db.table("challenge_submissions") \
        .select("*") \
        .eq("user_id", str(user.id)) \
        .eq("challenge_id", challenge_id) \
        .order("submitted_at", desc=True) \
        .execute()
    return result.data or []