import type { AssessmentPackV2, AssessmentGenerationContextV2, AssessmentResultV2, AssessmentSubmissionV2, CertificateDetailsV2, CertificationTrack, HealthStatusV2, GeneratedCourseV2, GeneratedProjectV2, GitHubValidationReportV2, IssuedCertificateV2, PaymentRequestResponseV2, ProjectEvaluationV2, PublicProfileV2, SmartQuestionV2, FreeCourseItem, FreeCourseCompletionPayload, YtAnalyzeResponse, YtAssessmentStartResponse, YtAssessmentGradeResult, } from '../types/getcertV2'; import { supabase } from './supabase'; import { DEFAULT_FREE_COURSES } from '../constants/defaultFreeCourses'; type ApiEnvelope = { success: boolean; data?: T; error?: string; details?: unknown; }; const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL || '').trim().replace(/\/$/, ''); function parsePositiveTimeoutMs(value: unknown, fallbackMs: number): number { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) { return fallbackMs; } return Math.round(parsed); } const API_REQUEST_TIMEOUT_MS = parsePositiveTimeoutMs(import.meta.env.VITE_API_REQUEST_TIMEOUT_MS, 120000); function normalizePath(path: string): string { return path.startsWith('/') ? path : `/${path}`; } function joinBaseAndPath(base: string, path: string): string { const normalizedPath = normalizePath(path); // Backward-compatible guard: if base already ends with /api and callers // pass /api/... paths, avoid constructing /api/api/... if (base.endsWith('/api') && normalizedPath.startsWith('/api/')) { return `${base}${normalizedPath.slice(4)}`; } return `${base}${normalizedPath}`; } function buildUrl(path: string): string { if (/^https?:\/\//i.test(path)) { return path; } if (!API_BASE_URL) { return normalizePath(path); } return joinBaseAndPath(API_BASE_URL, path); } function getNetworkErrorMessage(error: unknown): string { const message = error instanceof Error ? error.message : 'Unknown network error'; return `Unable to reach API endpoint. ${message}. In local dev, run both frontend and API servers (npm run dev:full) or verify VITE_API_BASE_URL.`; } async function getAuthorizationHeader(): Promise> { try { const { data } = await supabase.auth.getSession(); const accessToken = data.session?.access_token?.trim(); if (!accessToken) { return {}; } return { Authorization: `Bearer ${accessToken}` }; } catch { return {}; } } async function fetchWithTimeout(input: RequestInfo | URL, init: RequestInit = {}, timeoutMs = API_REQUEST_TIMEOUT_MS): Promise { const controller = new AbortController(); const timeout = window.setTimeout(() => controller.abort(), timeoutMs); try { return await fetch(input, { ...init, signal: controller.signal, }); } finally { window.clearTimeout(timeout); } } // No withFallback wrappers — all errors propagate to the UI for retry async function parseResponse(response: Response): Promise { const raw = await response.text(); let payload: ApiEnvelope | null = null; if (raw) { try { payload = JSON.parse(raw) as ApiEnvelope; } catch { payload = null; } } if (!response.ok) { if (payload?.error) { throw new Error(payload.error); } if (!payload) { throw new Error( `API ${response.status} returned non-JSON at ${response.url}. Run npm run dev:full or confirm VITE_API_BASE_URL.` ); } throw new Error(`API request failed with status ${response.status}`); } if (!payload || !payload.success || payload.data === undefined) { throw new Error('API returned an unexpected response shape'); } return payload.data; } async function postJson(path: string, body: unknown): Promise { let response: Response; try { const authorizationHeader = await getAuthorizationHeader(); response = await fetchWithTimeout(buildUrl(path), { method: 'POST', headers: { 'Content-Type': 'application/json', ...authorizationHeader }, body: JSON.stringify(body), }); } catch (error) { throw new Error(getNetworkErrorMessage(error)); } return parseResponse(response); } async function getJson(path: string): Promise { let response: Response; try { const authorizationHeader = await getAuthorizationHeader(); response = await fetchWithTimeout(buildUrl(path), { method: 'GET', headers: { 'Content-Type': 'application/json', ...authorizationHeader }, }); } catch (error) { throw new Error(getNetworkErrorMessage(error)); } return parseResponse(response); } let clientCachedFreeCourses: FreeCourseItem[] | null = null; export const getcertV2API = { generateCourse(skill: string): Promise { return postJson('/api/v2/course', { skill }); }, generateAssessment(skill: string, moduleTitles: string[], context?: AssessmentGenerationContextV2): Promise { return postJson('/api/v2/assessment', { mode: 'generate', skill, moduleTitles, courseSummary: context?.courseSummary, moduleContext: context?.modules, }); }, gradeAssessment(pack: AssessmentPackV2, submission: AssessmentSubmissionV2): Promise { return postJson('/api/v2/assessment', { mode: 'grade', pack, submission, }); }, generateProject(skill: string): Promise { return postJson('/api/v2/project', { skill }); }, validateGitHub(repoUrl: string): Promise { return postJson('/api/v2/github-validate', { repoUrl }); }, generateProjectQuestions( skill: string, url: string, retryCount = 0, isDesignProject = false ): Promise<{ questions: SmartQuestionV2[]; metadata: GitHubValidationReportV2['metadata']; validationSummary: { overallPassed: boolean; commitPassed: boolean; readmePassed: boolean; }; }> { return postJson('/api/v2/project-questions', { skill, repoUrl: isDesignProject ? undefined : url, portfolioUrl: isDesignProject ? url : undefined, retryCount, isDesignProject, }); }, evaluateProject(payload: { skill: string; repoUrl?: string; projectType: 'web' | 'python' | 'data' | 'design'; liveDemoUrl?: string; portfolioUrl?: string; notebookUrl?: string; questions: SmartQuestionV2[]; answers: Array<{ questionId: string; answer: string }>; }): Promise { return postJson('/api/v2/project-evaluate', payload); }, submitPaymentRequest(payload: { userId: string; txnId?: string; skill: string; projectTitle: string; githubUrl?: string; proofUrl: string; finalScore: number; scoreBreakdown?: ProjectEvaluationV2['scoreBreakdown']; validationSummary?: ProjectEvaluationV2['validationSummary']; projectType?: 'web' | 'python' | 'data' | 'design'; commitCount?: number; liveDemoUrl?: string; paymentRequestId?: string; track?: CertificationTrack; email?: string; phone?: string; promoCode?: string; amountInr?: number; }): Promise { return postJson('/api/v2/payment-submit', payload); }, getPaymentStatus( userId: string, paymentRequestId?: string, txnId?: string, skill?: string, projectTitle?: string, track?: string, ): Promise { const query = new URLSearchParams({ userId }); if (paymentRequestId) { query.set('paymentRequestId', paymentRequestId); } if (txnId) { query.set('txnId', txnId); } if (skill) { query.set('skill', skill); } if (projectTitle) { query.set('projectTitle', projectTitle); } if (track) { query.set('track', track); } return getJson( `/api/v2/payment-status?${query.toString()}` ); }, certify(payload: { userId: string; name: string; email: string; skill: string; projectTitle: string; githubUrl?: string; proofUrl: string; finalScore: number; scoreBreakdown?: ProjectEvaluationV2['scoreBreakdown']; validationSummary?: ProjectEvaluationV2['validationSummary']; projectType?: 'web' | 'python' | 'data' | 'design'; commitCount?: number; liveDemoUrl?: string; paymentRequestId?: string; txnId?: string; track?: CertificationTrack; }): Promise { return postJson('/api/v2/certify', payload); }, getCertificate(certificateId: string): Promise { return getJson(`/api/v2/certificate/${encodeURIComponent(certificateId)}`); }, getPublicProfile(userId: string): Promise { return getJson(`/api/v2/profile/${encodeURIComponent(userId)}`); }, getHealth(): Promise { return getJson('/api/v2/health'); }, async getFreeCourses(): Promise { if (clientCachedFreeCourses && clientCachedFreeCourses.length > 0) { return clientCachedFreeCourses; } // 1. Direct Supabase query (Lightning fast: 30-50ms directly from Supabase, zero server cold-start) try { const { data, error } = await supabase .from('free_courses') .select('*') .eq('is_active', true) .order('display_order', { ascending: true }); if (!error && data && data.length > 0) { const items = data.map((row: any) => { const videoId = row.youtube_video_id || 'kqtD5dpn9C8'; const maxStudents = typeof row.max_students === 'number' && row.max_students > 0 ? row.max_students : null; const certifiedCount = typeof row.certified_students_count === 'number' ? row.certified_students_count : 0; return { id: row.id, courseId: row.course_id, title: row.title, skill: row.skill, description: row.description || '', category: row.category || 'Programming', level: row.level || 'Beginner', durationMinutes: row.duration_minutes || 30, maxStudents, certifiedStudentsCount: certifiedCount, isSoldOut: Boolean(maxStudents !== null && certifiedCount >= maxStudents), remainingSlots: maxStudents !== null ? Math.max(0, maxStudents - certifiedCount) : null, youtubeUrl: row.youtube_url || `https://www.youtube.com/watch?v=${videoId}`, youtubeVideoId: videoId, channelTitle: row.channel_title || 'Programming with Mosh', thumbnailUrl: row.thumbnail_url || `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`, embedUrl: `https://www.youtube-nocookie.com/embed/${videoId}?rel=0`, isActive: Boolean(row.is_active ?? true), displayOrder: row.display_order ?? 0, keyTakeaways: Array.isArray(row.key_takeaways) ? row.key_takeaways : [], quizQuestions: Array.isArray(row.quiz_questions) ? row.quiz_questions : [], createdAt: row.created_at, updatedAt: row.updated_at, }; }); clientCachedFreeCourses = items; return items; } } catch { // Supabase query error or table not yet migrated } // 2. Fast API probe with 2000ms timeout (never hang for 120s on sleeping containers) try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 2000); const url = buildUrl('/api/v2/free-courses'); const response = await fetch(url, { signal: controller.signal }); clearTimeout(timeout); if (response.ok) { const payload = await response.json(); if (payload?.success && Array.isArray(payload.data?.courses) && payload.data.courses.length > 0) { clientCachedFreeCourses = payload.data.courses; return payload.data.courses; } } } catch { // Backend cold starting or unreachable } // 3. Instant local fallback (0ms) clientCachedFreeCourses = DEFAULT_FREE_COURSES; return DEFAULT_FREE_COURSES; }, async updateFreeCourseProgress(payload: { courseId: string; courseTitle: string; progressPercentage?: number; currentStep?: string; status?: string; score?: number; }): Promise<{ success: boolean }> { try { return await postJson<{ success: boolean }>('/api/v2/free-courses/progress', payload); } catch { return { success: false }; } }, completeFreeCourse(payload: FreeCourseCompletionPayload): Promise { return postJson('/api/v2/free-courses/complete', payload); }, // ─── GetCert YT ("Certify Your Learning from YouTube") ───────────────────── analyzeYouTubeCourse(url: string): Promise { return postJson('/api/v2/yt/analyze', { url }); }, startYtAssessment(snapshotId: string): Promise { return postJson('/api/v2/yt/assessment/start', { snapshotId }); }, submitYtAssessment(payload: { snapshotId: string; attemptId: string; answers: Record; }): Promise { return postJson('/api/v2/yt/assessment/submit', payload); }, checkoutYtCertification(payload: { snapshotId: string; score: number; email?: string; }): Promise<{ paywallEnabled: boolean; amountInr: number; checkoutUrl: string; checkoutId: string; paymentSessionId: string; track: string; message: string; }> { return postJson('/api/v2/yt/checkout', payload); }, certifyYtCourse(payload: { userId: string; name: string; email: string; skill: string; projectTitle: string; finalScore: number; paymentRequestId?: string; txnId?: string; track: 'ytcert'; extraMetadata?: Record; }): Promise { return postJson('/api/v2/certify', payload); }, };