import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:latlong2/latlong.dart'; import '../auth/telegram_session.dart' as telegram_auth; import '../models/place_models.dart'; class MapflowApi { MapflowApi({ http.Client? client, String? telegramInitData, String? telegramLoginData, String? mapflowSessionToken, String endpoint = const String.fromEnvironment( 'API_BASE_URL', defaultValue: '/graphql', ), }) : _client = client ?? http.Client(), _telegramInitData = telegramInitData ?? telegram_auth.telegramInitData(), _telegramLoginData = telegramLoginData ?? telegram_auth.telegramLoginData(), _mapflowSessionToken = mapflowSessionToken ?? telegram_auth.mapflowSessionToken(), _endpoint = Uri.base.resolve(endpoint); final http.Client _client; final String _telegramInitData; final String _telegramLoginData; final String _mapflowSessionToken; final Uri _endpoint; bool get hasTelegramAuth => _telegramInitData.isNotEmpty || _telegramLoginData.isNotEmpty || _mapflowSessionToken.isNotEmpty; Future authenticateTelegram() async { if (!hasTelegramAuth) { return null; } if (_mapflowSessionToken.isNotEmpty) { final data = await _graphql(''' query Me { me { id telegramId username firstName lastName photoUrl languageCode } } '''); return AppUser.fromJson(data['me'] as Map); } if (_telegramLoginData.isNotEmpty) { final loginData = jsonDecode(_telegramLoginData) as Map; final data = await _graphql( ''' mutation AuthenticateTelegramLogin( \$input: AuthenticateTelegramLoginInput! ) { authenticateTelegramLogin(input: \$input) { user { id telegramId username firstName lastName photoUrl languageCode } } } ''', variables: {'input': loginData}, ); final payload = data['authenticateTelegramLogin'] as Map; final user = payload['user'] as Map; return AppUser.fromJson(user); } final data = await _graphql( ''' mutation AuthenticateTelegram(\$input: AuthenticateTelegramInput!) { authenticateTelegram(input: \$input) { user { id telegramId username firstName lastName photoUrl languageCode } } } ''', variables: { 'input': {'initData': _telegramInitData}, }, ); final payload = data['authenticateTelegram'] as Map; final user = payload['user'] as Map; return AppUser.fromJson(user); } Future startTelegramBotLogin() async { final data = await _graphql(''' mutation StartTelegramBotLogin { startTelegramBotLogin { token botUrl expiresAt } } '''); return TelegramBotLogin.fromJson( data['startTelegramBotLogin'] as Map, ); } Future fetchTelegramBotLoginStatus( String token, ) async { final data = await _graphql( ''' query TelegramBotLoginStatus(\$token: String!) { telegramBotLoginStatus(token: \$token) { status sessionToken user { id telegramId username firstName lastName photoUrl languageCode } } } ''', variables: {'token': token}, ); return TelegramBotLoginStatus.fromJson( data['telegramBotLoginStatus'] as Map, ); } Future> fetchPlaces() async { final data = await _graphql(''' query Places { places { id googlePlaceId name latitude longitude experiences { id status analysis createdAt } } } '''); final places = data['places'] as List; return places.map((item) { final place = item as Map; return PlaceRecommendation( id: place['id'] as String, googlePlaceId: place['googlePlaceId'] as String, name: place['name'] as String, area: '', photoUrls: const [], coordinate: LatLng( (place['latitude'] as num).toDouble(), (place['longitude'] as num).toDouble(), ), traits: _traitsFromExperiences(place['experiences'] as List), ); }).toList(); } Future> fetchNearbyPlaces({ required LatLng coordinate, required int radiusMeters, }) async { final data = await _graphql( ''' query NearbyPlaces(\$input: NearbyPlacesInput!) { nearbyPlaces(input: \$input) { id googlePlaceId name latitude longitude experiences { id status analysis createdAt } } } ''', variables: { 'input': { 'latitude': coordinate.latitude, 'longitude': coordinate.longitude, 'radiusMeters': radiusMeters, }, }, ); final places = data['nearbyPlaces'] as List; return places.map((item) { final place = item as Map; return PlaceRecommendation( id: place['id'] as String, googlePlaceId: place['googlePlaceId'] as String, name: place['name'] as String, area: '', photoUrls: const [], coordinate: LatLng( (place['latitude'] as num).toDouble(), (place['longitude'] as num).toDouble(), ), traits: _traitsFromExperiences(place['experiences'] as List), ); }).toList(); } Future createVoiceExperience({ required String googlePlaceId, required String googleName, required LatLng coordinate, required int durationSeconds, required String audioObjectKey, }) async { if (!hasTelegramAuth) { throw StateError('Telegram authorization is required.'); } await _graphql( ''' mutation CreateVoiceExperience(\$input: CreateVoiceExperienceInput!) { createVoiceExperience(input: \$input) { id } } ''', variables: { 'input': { 'googlePlaceId': googlePlaceId, 'googleName': googleName, 'latitude': coordinate.latitude, 'longitude': coordinate.longitude, 'durationSeconds': durationSeconds, 'audioObjectKey': audioObjectKey, }, }, ); } Future> _graphql( String query, { Map? variables, }) async { final response = await _client.post( _endpoint, headers: { 'content-type': 'application/json', if (_telegramInitData.isNotEmpty) 'x-telegram-init-data': _telegramInitData, if (_telegramLoginData.isNotEmpty) 'x-telegram-login-data': _telegramLoginData, if (_mapflowSessionToken.isNotEmpty) 'x-mapflow-session-token': _mapflowSessionToken, }, body: jsonEncode({'query': query, 'variables': variables ?? {}}), ); if (response.statusCode < 200 || response.statusCode >= 300) { throw StateError('GraphQL request failed with ${response.statusCode}.'); } final payload = jsonDecode(response.body) as Map; final errors = payload['errors']; if (errors is List && errors.isNotEmpty) { throw StateError(jsonEncode(errors)); } return payload['data'] as Map; } Set _traitsFromExperiences(List experiences) { final traits = {}; for (final item in experiences) { final experience = item as Map; final analysis = experience['analysis']; if (analysis is! Map) { continue; } final tags = analysis['tags']; if (tags is! List) { continue; } for (final tag in tags) { final trait = _traitByTag(tag.toString()); if (trait != null) { traits.add(trait); } } } return traits; } PlaceTrait? _traitByTag(String tag) { final name = tag.contains(':') ? tag.split(':').last : tag; for (final trait in PlaceTrait.values) { if (trait.name == name) { return trait; } } return null; } }