import { NextResponse } from 'next/server'; import { getAdminAuth, getAdminDb } from '@/lib/firebase/admin'; import { FieldValue } from 'firebase-admin/firestore'; /** * Associate existing sessions with a project when GitHub is connected * Matches sessions by: * 1. githubRepo field (from Cursor extension) * 2. workspacePath (if repo name matches) */ export async function POST( request: Request, { params }: { params: Promise<{ projectId: string }> } ) { try { const { projectId } = await params; const authHeader = request.headers.get('Authorization'); if (!authHeader?.startsWith('Bearer ')) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const idToken = authHeader.split('Bearer ')[1]; const adminAuth = getAdminAuth(); const adminDb = getAdminDb(); let userId: string; try { const decodedToken = await adminAuth.verifyIdToken(idToken); userId = decodedToken.uid; } catch (error) { return NextResponse.json({ error: 'Invalid token' }, { status: 401 }); } const { githubRepo, githubRepoUrl } = await request.json(); if (!githubRepo) { return NextResponse.json( { error: 'githubRepo is required' }, { status: 400 } ); } // Verify project belongs to user const projectDoc = await adminDb.collection('projects').doc(projectId).get(); if (!projectDoc.exists || projectDoc.data()?.userId !== userId) { return NextResponse.json( { error: 'Project not found or unauthorized' }, { status: 403 } ); } const projectData = projectDoc.data(); const projectWorkspacePath = projectData?.workspacePath; console.log(`[Associate GitHub Sessions] Project: ${projectId}`); console.log(`[Associate GitHub Sessions] GitHub repo: ${githubRepo}`); console.log(`[Associate GitHub Sessions] Project workspace path: ${projectWorkspacePath || 'not set'}`); console.log(`[Associate GitHub Sessions] User ID: ${userId}`); // Strategy 1: Match by exact githubRepo field in sessions // (This requires the Cursor extension to send githubRepo with sessions) const sessionsSnapshot1 = await adminDb .collection('sessions') .where('userId', '==', userId) .where('githubRepo', '==', githubRepo) .where('needsProjectAssociation', '==', true) .get(); console.log(`[Associate GitHub Sessions] Found ${sessionsSnapshot1.size} sessions with exact githubRepo match`); // Strategy 2: Match by exact workspacePath (if project has one set) let matchedByPath: any[] = []; if (projectWorkspacePath) { console.log(`[Associate GitHub Sessions] Strategy 2A: Exact workspace path match`); console.log(`[Associate GitHub Sessions] Looking for sessions from: ${projectWorkspacePath}`); const pathMatchSnapshot = await adminDb .collection('sessions') .where('userId', '==', userId) .where('workspacePath', '==', projectWorkspacePath) .where('needsProjectAssociation', '==', true) .get(); matchedByPath = pathMatchSnapshot.docs; console.log(`[Associate GitHub Sessions] Found ${matchedByPath.length} sessions with exact workspace path match`); } else { // Fallback: Match by repo name (less reliable but better than nothing) console.log(`[Associate GitHub Sessions] Strategy 2B: Fuzzy match by repo folder name (project has no workspace path set)`); const repoName = githubRepo.split('/')[1]; // Extract "my-app" from "username/my-app" console.log(`[Associate GitHub Sessions] Looking for folders ending with: ${repoName}`); const allUnassociatedSessions = await adminDb .collection('sessions') .where('userId', '==', userId) .where('needsProjectAssociation', '==', true) .get(); console.log(`[Associate GitHub Sessions] Total unassociated sessions for user: ${allUnassociatedSessions.size}`); matchedByPath = allUnassociatedSessions.docs.filter(doc => { const workspacePath = doc.data().workspacePath; if (!workspacePath) return false; const pathSegments = workspacePath.split('/'); const lastSegment = pathSegments[pathSegments.length - 1]; const matches = lastSegment === repoName; if (matches) { console.log(`[Associate GitHub Sessions] ✅ Fuzzy match: ${workspacePath} ends with ${repoName}`); } return matches; }); console.log(`[Associate GitHub Sessions] Found ${matchedByPath.length} sessions with fuzzy folder name match`); // Debug: Log some example workspace paths to help diagnose if (matchedByPath.length === 0 && allUnassociatedSessions.size > 0) { console.log(`[Associate GitHub Sessions] Debug - Example workspace paths in unassociated sessions:`); allUnassociatedSessions.docs.slice(0, 5).forEach(doc => { const path = doc.data().workspacePath; const folder = path ? path.split('/').pop() : 'null'; console.log(` - ${path} (folder: ${folder})`); }); console.log(`[Associate GitHub Sessions] Tip: Set project.workspacePath for accurate matching`); } } // Combine both strategies (deduplicate by session ID) const allMatchedSessions = new Map(); // Add exact matches sessionsSnapshot1.docs.forEach(doc => { allMatchedSessions.set(doc.id, doc); }); // Add path matches matchedByPath.forEach(doc => { allMatchedSessions.set(doc.id, doc); }); // Batch update all matched sessions if (allMatchedSessions.size > 0) { const batch = adminDb.batch(); let count = 0; allMatchedSessions.forEach((doc) => { batch.update(doc.ref, { projectId, needsProjectAssociation: false, updatedAt: FieldValue.serverTimestamp(), }); count++; }); await batch.commit(); console.log(`[Associate GitHub Sessions] Successfully associated ${count} sessions with project ${projectId}`); return NextResponse.json({ success: true, sessionsAssociated: count, message: `Found and linked ${count} existing chat sessions from this repository`, details: { exactMatches: sessionsSnapshot1.size, pathMatches: matchedByPath.length, } }); } return NextResponse.json({ success: true, sessionsAssociated: 0, message: 'No matching sessions found for this repository', }); } catch (error) { console.error('[Associate GitHub Sessions] Error:', error); return NextResponse.json( { error: 'Failed to associate sessions', details: error instanceof Error ? error.message : String(error), }, { status: 500 } ); } }