import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:flutter_dotenv/flutter_dotenv.dart'; import '../models/activity.dart'; import '../services/error_service.dart'; /// Service pour rechercher des activités touristiques via Google Places API class ActivityPlacesService { static final ActivityPlacesService _instance = ActivityPlacesService._internal(); factory ActivityPlacesService() => _instance; ActivityPlacesService._internal(); final ErrorService _errorService = ErrorService(); static final String _apiKey = dotenv.env['GOOGLE_MAPS_API_KEY'] ?? ''; /// Recherche des activités près d'une destination Future> searchActivities({ required String destination, required String tripId, ActivityCategory? category, int radius = 5000, }) async { try { print('ActivityPlacesService: Recherche d\'activités pour: $destination'); // 1. Géocoder la destination final coordinates = await _geocodeDestination(destination); if (coordinates == null) { throw Exception('Impossible de localiser la destination: $destination'); } // 2. Rechercher les activités par catégorie ou toutes les catégories List allActivities = []; if (category != null) { final activities = await _searchByCategory( coordinates['lat']!, coordinates['lng']!, category, tripId, radius, ); allActivities.addAll(activities); } else { // Rechercher dans toutes les catégories principales final mainCategories = [ ActivityCategory.attraction, ActivityCategory.museum, ActivityCategory.restaurant, ActivityCategory.culture, ActivityCategory.nature, ]; for (final cat in mainCategories) { final activities = await _searchByCategory( coordinates['lat']!, coordinates['lng']!, cat, tripId, radius, ); allActivities.addAll(activities); } } // 3. Supprimer les doublons et trier par note final uniqueActivities = _removeDuplicates(allActivities); uniqueActivities.sort((a, b) => (b.rating ?? 0).compareTo(a.rating ?? 0)); print('ActivityPlacesService: ${uniqueActivities.length} activités trouvées'); return uniqueActivities.take(50).toList(); // Limiter à 50 résultats } catch (e) { print('ActivityPlacesService: Erreur lors de la recherche: $e'); _errorService.logError('activity_places_service', e); return []; } } /// Géocode une destination pour obtenir les coordonnées Future?> _geocodeDestination(String destination) async { try { final encodedDestination = Uri.encodeComponent(destination); final url = 'https://maps.googleapis.com/maps/api/geocode/json?address=$encodedDestination&key=$_apiKey'; final response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { final data = json.decode(response.body); if (data['status'] == 'OK' && data['results'].isNotEmpty) { final location = data['results'][0]['geometry']['location']; return { 'lat': location['lat'].toDouble(), 'lng': location['lng'].toDouble(), }; } } return null; } catch (e) { print('ActivityPlacesService: Erreur géocodage: $e'); return null; } } /// Recherche des activités par catégorie Future> _searchByCategory( double lat, double lng, ActivityCategory category, String tripId, int radius, ) async { try { final url = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json' '?location=$lat,$lng' '&radius=$radius' '&type=${category.googlePlaceType}' '&key=$_apiKey'; final response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { final data = json.decode(response.body); if (data['status'] == 'OK') { final List activities = []; for (final place in data['results']) { try { final activity = await _convertPlaceToActivity(place, tripId, category); if (activity != null) { activities.add(activity); } } catch (e) { print('ActivityPlacesService: Erreur conversion place: $e'); } } return activities; } } return []; } catch (e) { print('ActivityPlacesService: Erreur recherche par catégorie: $e'); return []; } } /// Convertit un résultat Google Places en Activity Future _convertPlaceToActivity( Map place, String tripId, ActivityCategory category, ) async { try { final placeId = place['place_id']; if (placeId == null) return null; // Récupérer les détails supplémentaires final details = await _getPlaceDetails(placeId); final geometry = place['geometry']?['location']; final photos = place['photos'] as List?; // Obtenir une image de qualité String? imageUrl; if (photos != null && photos.isNotEmpty) { final photoReference = photos.first['photo_reference']; imageUrl = 'https://maps.googleapis.com/maps/api/place/photo' '?maxwidth=800' '&photoreference=$photoReference' '&key=$_apiKey'; } return Activity( id: '', // Sera généré lors de la sauvegarde tripId: tripId, name: place['name'] ?? 'Activité inconnue', description: details?['editorial_summary']?['overview'] ?? details?['formatted_address'] ?? 'Découvrez cette activité incontournable !', category: category.displayName, imageUrl: imageUrl, rating: place['rating']?.toDouble(), priceLevel: _getPriceLevelString(place['price_level']), address: details?['formatted_address'] ?? place['vicinity'], latitude: geometry?['lat']?.toDouble(), longitude: geometry?['lng']?.toDouble(), placeId: placeId, website: details?['website'], phoneNumber: details?['formatted_phone_number'], openingHours: _parseOpeningHours(details?['opening_hours']), votes: {}, createdAt: DateTime.now(), updatedAt: DateTime.now(), ); } catch (e) { print('ActivityPlacesService: Erreur conversion place: $e'); return null; } } /// Récupère les détails d'un lieu Future?> _getPlaceDetails(String placeId) async { try { final url = 'https://maps.googleapis.com/maps/api/place/details/json' '?place_id=$placeId' '&fields=formatted_address,formatted_phone_number,website,opening_hours,editorial_summary' '&key=$_apiKey'; final response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { final data = json.decode(response.body); if (data['status'] == 'OK') { return data['result']; } } return null; } catch (e) { print('ActivityPlacesService: Erreur récupération détails: $e'); return null; } } /// Convertit le niveau de prix en string String? _getPriceLevelString(int? priceLevel) { if (priceLevel == null) return null; final level = PriceLevel.fromLevel(priceLevel); return level?.displayName; } /// Parse les heures d'ouverture List _parseOpeningHours(Map? openingHours) { if (openingHours == null) return []; final weekdayText = openingHours['weekday_text'] as List?; if (weekdayText == null) return []; return weekdayText.cast(); } /// Supprime les doublons basés sur le placeId List _removeDuplicates(List activities) { final seen = {}; return activities.where((activity) { if (activity.placeId == null) return true; if (seen.contains(activity.placeId)) return false; seen.add(activity.placeId!); return true; }).toList(); } /// Recherche d'activités par texte libre Future> searchActivitiesByText({ required String query, required String destination, required String tripId, int radius = 5000, }) async { try { print('ActivityPlacesService: Recherche textuelle: $query à $destination'); // Géocoder la destination final coordinates = await _geocodeDestination(destination); if (coordinates == null) { throw Exception('Impossible de localiser la destination'); } final encodedQuery = Uri.encodeComponent(query); final url = 'https://maps.googleapis.com/maps/api/place/textsearch/json' '?query=$encodedQuery in $destination' '&location=${coordinates['lat']},${coordinates['lng']}' '&radius=$radius' '&key=$_apiKey'; final response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { final data = json.decode(response.body); if (data['status'] == 'OK') { final List activities = []; for (final place in data['results']) { try { // Déterminer la catégorie basée sur les types du lieu final types = List.from(place['types'] ?? []); final category = _determineCategoryFromTypes(types); final activity = await _convertPlaceToActivity(place, tripId, category); if (activity != null) { activities.add(activity); } } catch (e) { print('ActivityPlacesService: Erreur conversion place: $e'); } } return activities; } } return []; } catch (e) { print('ActivityPlacesService: Erreur recherche textuelle: $e'); return []; } } /// Détermine la catégorie d'activité basée sur les types Google Places ActivityCategory _determineCategoryFromTypes(List types) { for (final type in types) { for (final category in ActivityCategory.values) { if (category.googlePlaceType == type) { return category; } } } // Catégories par défaut basées sur des types communs if (types.contains('restaurant') || types.contains('food')) { return ActivityCategory.restaurant; } else if (types.contains('museum')) { return ActivityCategory.museum; } else if (types.contains('park')) { return ActivityCategory.nature; } else if (types.contains('shopping_mall') || types.contains('store')) { return ActivityCategory.shopping; } else if (types.contains('night_club') || types.contains('bar')) { return ActivityCategory.nightlife; } return ActivityCategory.attraction; // Par défaut } }