'invalid_plan_id'], 400); } try { $database = Database::getInstance(); $connection = $database->getConnection(); if ($planUuid !== null) { $planQuery = $database->prepare( 'SELECT plan_uuid, slug, title_ar, summary_ar, goal, level, weeks_duration, source_notes, safety_notes_ar, content_revision FROM training_plans WHERE is_published = 1 AND plan_uuid = ? LIMIT 1' ); $planQuery->bind_param('s', $planUuid); } else { $planQuery = $database->prepare( 'SELECT plan_uuid, slug, title_ar, summary_ar, goal, level, weeks_duration, source_notes, safety_notes_ar, content_revision FROM training_plans WHERE is_published = 1 ORDER BY title_ar' ); } $planQuery->execute(); $planResult = $planQuery->get_result(); $plans = []; while ($plan = $planResult->fetch_assoc()) { $plan['weeks_duration'] = (int) $plan['weeks_duration']; $plan['content_revision'] = (int) $plan['content_revision']; $plan['source_notes'] = json_decode($plan['source_notes'] ?? '[]', true) ?? []; $plan['safety_notes_ar'] = json_decode($plan['safety_notes_ar'], true) ?? []; $plans[$plan['plan_uuid']] = $plan; } $planQuery->close(); if ($planUuid !== null && !$plans) { api_json(['error' => 'plan_not_found'], 404); } if (!$plans) { header('Cache-Control: public, max-age=60, stale-while-revalidate=300'); api_json(['plans' => []]); } $planUuids = array_keys($plans); $ids = array_fill(0, count($planUuids), '?'); $sessionQuery = $database->prepare( 'SELECT s.plan_id, p.plan_uuid, s.week_number, s.day_number, s.title_ar, s.session_type, s.duration_minutes, s.intensity, s.notes_ar, e.exercise_uuid, e.slug AS exercise_slug, e.title_ar AS exercise_title_ar, e.instructions_ar, e.target_muscles, e.equipment, e.gif_url, e.gif_poster_url, e.duration_seconds AS exercise_duration_seconds, e.repetitions, e.safety_notes_ar AS exercise_safety_notes_ar, alt.exercise_uuid AS alternative_exercise_uuid, se.sort_order AS exercise_sort_order, se.sets, se.reps, se.duration_seconds, se.rest_seconds FROM training_plan_sessions s JOIN training_plans p ON p.id = s.plan_id LEFT JOIN training_session_exercises se ON se.session_id = s.id LEFT JOIN exercises e ON e.id = se.exercise_id AND e.is_published = 1 LEFT JOIN exercises alt ON alt.id = e.alternative_exercise_id AND alt.is_published = 1 WHERE p.plan_uuid IN (' . implode(',', $ids) . ') AND p.is_published = 1 ORDER BY p.title_ar, s.week_number, s.day_number, se.sort_order' ); $bindArguments = [str_repeat('s', count($planUuids))]; foreach ($planUuids as $index => $_planUuid) { $bindArguments[] = &$planUuids[$index]; } call_user_func_array([$sessionQuery, 'bind_param'], $bindArguments); $sessionQuery->execute(); $sessionResult = $sessionQuery->get_result(); foreach ($plans as &$plan) { $plan['sessions'] = []; } unset($plan); while ($row = $sessionResult->fetch_assoc()) { $uuid = $row['plan_uuid']; $week = (int) $row['week_number']; $day = (int) $row['day_number']; $key = $week . ':' . $day; if (!isset($plans[$uuid]['sessions'][$key])) { $plans[$uuid]['sessions'][$key] = [ 'week' => $week, 'day' => $day, 'title_ar' => $row['title_ar'], 'type' => $row['session_type'], 'duration_minutes' => (int) $row['duration_minutes'], 'intensity' => $row['intensity'], 'notes_ar' => $row['notes_ar'], 'exercises' => [], ]; } if ($row['exercise_uuid'] !== null) { $plans[$uuid]['sessions'][$key]['exercises'][] = [ 'exercise_uuid' => $row['exercise_uuid'], 'slug' => $row['exercise_slug'], 'title_ar' => $row['exercise_title_ar'], 'instructions_ar' => json_decode($row['instructions_ar'], true) ?? [], 'target_muscles' => json_decode($row['target_muscles'], true) ?? [], 'equipment' => json_decode($row['equipment'], true) ?? [], 'gif_url' => $row['gif_url'], 'gif_poster_url' => $row['gif_poster_url'], 'duration_seconds' => $row['duration_seconds'] === null ? null : (int) $row['duration_seconds'], 'repetitions' => $row['repetitions'], 'safety_notes_ar' => json_decode($row['exercise_safety_notes_ar'] ?? '[]', true) ?? [], 'alternative_exercise_uuid' => $row['alternative_exercise_uuid'], 'prescription' => [ 'sets' => $row['sets'] === null ? null : (int) $row['sets'], 'reps' => $row['reps'], 'duration_seconds' => $row['duration_seconds'] === null ? null : (int) $row['duration_seconds'], 'rest_seconds' => (int) $row['rest_seconds'], ], ]; } } $sessionQuery->close(); foreach ($plans as &$plan) { $plan['sessions'] = array_values($plan['sessions']); } unset($plan); header('Cache-Control: public, max-age=60, stale-while-revalidate=300'); api_json(['plans' => array_values($plans)]); } catch (Throwable $exception) { error_log('Training plans unavailable: ' . $exception->getMessage()); api_json(['error' => 'service_unavailable'], 503); }