import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import '../models/training_plan.dart'; import 'auth_service.dart'; class TrainingPlanService { TrainingPlanService({required AuthService auth, http.Client? client}) : _auth = auth, _client = client ?? http.Client(); static const _cacheKey = 'sportpath_training_plans_v1'; final AuthService _auth; final http.Client _client; Future> readCachedPlans() async { final prefs = await SharedPreferences.getInstance(); return _decodeCache(prefs.getString(_cacheKey)); } Future fetchPlans({bool refresh = false}) async { final prefs = await SharedPreferences.getInstance(); final cached = _decodeCache(prefs.getString(_cacheKey)); if (!_auth.isApiConfigured) { return TrainingCatalogResult( plans: cached, fromCache: cached.isNotEmpty, message: 'عنوان الخادم غير مضبوط على هذه النسخة.', ); } try { final response = await _client .get(Uri.parse('${_auth.apiBase}/plans.php')) .timeout(const Duration(seconds: 15)); if (response.statusCode != 200) { throw TrainingCatalogException( 'تعذر تحميل الخطط (${response.statusCode}).'); } final decoded = jsonDecode(response.body); if (decoded is! Map || decoded['plans'] is! List) { throw const TrainingCatalogException('صيغة بيانات الخطط غير متوقعة.'); } final plans = (decoded['plans'] as List) .whereType>() .map(TrainingPlan.fromJson) .where((plan) => plan.uuid.isNotEmpty) .toList(growable: false); // An empty catalog can be an unpublished/temporarily incomplete response. // Keep the last known good catalog rather than erasing offline content. if (plans.isEmpty && cached.isNotEmpty) { return TrainingCatalogResult( plans: cached, fromCache: true, message: 'لا توجد خطط منشورة حاليًا؛ نعرض آخر نسخة محفوظة.', ); } await prefs.setString( _cacheKey, jsonEncode(plans.map(_toCacheJson).toList(growable: false)), ); return TrainingCatalogResult(plans: plans, fromCache: false); } catch (error) { if (cached.isNotEmpty) { return TrainingCatalogResult( plans: cached, fromCache: true, message: 'تعذر الاتصال؛ نعرض الخطط المحفوظة على الجهاز.', ); } return TrainingCatalogResult( plans: const [], fromCache: false, message: error is TrainingCatalogException ? error.message : 'لا يوجد اتصال بالخادم الآن.', ); } } List _decodeCache(String? value) { if (value == null || value.isEmpty) return const []; try { final decoded = jsonDecode(value); if (decoded is! List) return const []; return decoded .whereType>() .map(TrainingPlan.fromJson) .toList(growable: false); } catch (_) { return const []; } } Map _toCacheJson(TrainingPlan plan) => { 'plan_uuid': plan.uuid, 'slug': plan.slug, 'title_ar': plan.title, 'summary_ar': plan.summary, 'goal': plan.goal, 'level': plan.level, 'weeks_duration': plan.weeksDuration, 'safety_notes_ar': plan.safetyNotes, 'sessions': plan.sessions .map((session) => { 'week': session.week, 'day': session.day, 'title_ar': session.title, 'type': session.type, 'duration_minutes': session.durationMinutes, 'intensity': session.intensity, 'notes_ar': session.notes, 'exercises': session.exercises .map((exercise) => { 'exercise_uuid': exercise.uuid, 'slug': exercise.slug, 'title_ar': exercise.title, 'instructions_ar': exercise.instructions, 'target_muscles': exercise.targetMuscles, 'equipment': exercise.equipment, 'gif_url': exercise.gifUrl, 'gif_poster_url': exercise.posterUrl, 'duration_seconds': exercise.durationSeconds, 'repetitions': exercise.repetitions, 'safety_notes_ar': exercise.safetyNotes, 'alternative_exercise_uuid': exercise.alternativeUuid, 'prescription': { 'sets': exercise.prescription.sets, 'reps': exercise.prescription.reps, 'duration_seconds': exercise.prescription.durationSeconds, 'rest_seconds': exercise.prescription.restSeconds, }, }) .toList(growable: false), }) .toList(growable: false), }; } class TrainingCatalogException implements Exception { const TrainingCatalogException(this.message); final String message; }