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 []