import { NextResponse } from 'next/server'; import { PrismaClient } from '@prisma/client'; import { getLocalizedValue } from '@/lib/i18n'; import type { GameState, Statistics } from '@/lib/gameState'; const prisma = new PrismaClient(); /** * Validate UUID format (basic check) * Supports both legacy format (single UUID) and new format (basePlayerId:deviceId) */ function isValidPlayerId(playerId: string): boolean { // Legacy format: single UUID const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; // New format: basePlayerId:deviceId (two UUIDs separated by colon) const combinedRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; return uuidRegex.test(playerId) || combinedRegex.test(playerId); } /** * Extract base player ID from full player ID * Format: {basePlayerId}:{deviceId} -> {basePlayerId} * Legacy: {uuid} -> {uuid} */ function extractBasePlayerId(fullPlayerId: string): string { const colonIndex = fullPlayerId.indexOf(':'); if (colonIndex === -1) { // Legacy format (no device ID) - return as is return fullPlayerId; } return fullPlayerId.substring(0, colonIndex); } /** * GET /api/player-state * * Loads player state for a given identifier and genre/special. * * Query parameters: * - genre: Genre name (e.g., "Rock") * - special: Special name (e.g., "00725") * * Headers: * - X-Player-Id: Player identifier (UUID) */ export async function GET(request: Request) { try { const { searchParams } = new URL(request.url); const genreName = searchParams.get('genre'); const specialName = searchParams.get('special'); // Get player identifier from header const playerId = request.headers.get('X-Player-Id'); if (!playerId || !isValidPlayerId(playerId)) { return NextResponse.json( { error: 'Invalid or missing player identifier' }, { status: 400 } ); } // Determine genre key let genreKey: string; if (specialName) { genreKey = `special:${specialName}`; } else if (genreName) { genreKey = genreName; } else { genreKey = 'global'; } // Load player state from database const playerState = await prisma.playerState.findUnique({ where: { identifier_genreKey: { identifier: playerId, genreKey: genreKey, }, }, }); if (!playerState) { return NextResponse.json(null, { status: 404 }); } // Parse JSON strings let gameState: GameState; let statistics: Statistics; try { gameState = JSON.parse(playerState.gameState) as GameState; statistics = JSON.parse(playerState.statistics) as Statistics; } catch (parseError) { console.error('[player-state] Failed to parse stored state:', parseError); return NextResponse.json( { error: 'Invalid stored state format' }, { status: 500 } ); } return NextResponse.json({ gameState, statistics, }); } catch (error) { console.error('[player-state] Error loading player state:', error); return NextResponse.json( { error: 'Internal Server Error' }, { status: 500 } ); } } /** * POST /api/player-state * * Saves player state for a given identifier and genre/special. * * Request body: * - genreKey: Genre key (e.g., "global", "Rock", "special:00725") * - gameState: GameState object * - statistics: Statistics object * * Headers: * - X-Player-Id: Player identifier (UUID) */ export async function POST(request: Request) { try { // Get player identifier from header const playerId = request.headers.get('X-Player-Id'); if (!playerId || !isValidPlayerId(playerId)) { return NextResponse.json( { error: 'Invalid or missing player identifier' }, { status: 400 } ); } // Parse request body const body = await request.json(); const { genreKey, gameState, statistics } = body; if (!genreKey || !gameState || !statistics) { return NextResponse.json( { error: 'Missing required fields: genreKey, gameState, statistics' }, { status: 400 } ); } // Validate genre key format if (typeof genreKey !== 'string' || genreKey.length === 0) { return NextResponse.json( { error: 'Invalid genreKey format' }, { status: 400 } ); } // Serialize to JSON strings const gameStateJson = JSON.stringify(gameState); const statisticsJson = JSON.stringify(statistics); // Upsert player state (update if exists, create if not) await prisma.playerState.upsert({ where: { identifier_genreKey: { identifier: playerId, genreKey: genreKey, }, }, update: { gameState: gameStateJson, statistics: statisticsJson, lastPlayed: new Date(), }, create: { identifier: playerId, genreKey: genreKey, gameState: gameStateJson, statistics: statisticsJson, }, }); return NextResponse.json({ success: true }); } catch (error) { console.error('[player-state] Error saving player state:', error); return NextResponse.json( { error: 'Internal Server Error' }, { status: 500 } ); } }