import 'dart:convert'; import 'dart:io'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; /// HTTP client for backend API calls /// Replaces direct Isar DB access with API endpoints class BackendClient { final String backendUrl; final String apiKey; final HttpClient _httpClient; BackendClient({ required this.backendUrl, required this.apiKey, }) : _httpClient = HttpClient(); /// Make HTTP request with retry logic Future> _request({ required String method, required String path, Map? body, int maxRetries = 3, Duration timeout = const Duration(seconds: 10), }) async { Exception? lastException; for (int attempt = 0; attempt < maxRetries; attempt++) { try { final uri = Uri.parse('$backendUrl/api/v2$path'); final request = await _httpClient.openUrl(method, uri) .timeout(timeout); request.headers.contentType = ContentType.json; request.headers.set('X-API-Key', apiKey); if (body != null) { request.write(jsonEncode(body)); } final response = await request.close().timeout(timeout); final responseBody = await response .transform(utf8.decoder) .join() .timeout(timeout); if (response.statusCode >= 200 && response.statusCode < 300) { if (responseBody.isEmpty) { return {}; } return jsonDecode(responseBody) as Map; } else { throw HttpException( 'HTTP ${response.statusCode}: $responseBody', uri: uri, ); } } catch (e) { lastException = e is Exception ? e : Exception(e.toString()); if (attempt < maxRetries - 1) { // Exponential backoff: 1s, 2s, 4s await Future.delayed(Duration(seconds: 1 << attempt)); } } } throw lastException ?? Exception('Request failed after $maxRetries attempts'); } /// Generate a Telegram auth code for a user /// Calls backend API to generate the code Future generateAuthCode({ required String telegramUserId, String? telegramUsername, String? firstName, String? lastName, }) async { try { final response = await _request( method: 'POST', path: '/auth/telegram/generate-code', body: { 'telegramUserId': telegramUserId, if (telegramUsername != null) 'telegramUsername': telegramUsername, if (firstName != null) 'firstName': firstName, if (lastName != null) 'lastName': lastName, }, ); return response['code'] as String?; } catch (e, s) { print('Error generating Telegram auth code: $e\n$s'); return null; } } /// Claim a web-generated Telegram auth code for a user. /// Returns true when the backend confirmed the claim. Future claimWebAuthCode({ required String code, required String telegramUserId, String? telegramUsername, String? firstName, String? lastName, }) async { try { await _request( method: 'POST', path: '/auth/telegram/claim-code', body: { 'code': code, 'telegramUserId': telegramUserId, if (telegramUsername != null) 'telegramUsername': telegramUsername, if (firstName != null) 'firstName': firstName, if (lastName != null) 'lastName': lastName, }, ); return true; } catch (e, s) { print('Error claiming Telegram auth code: $e\n$s'); return false; } } /// Check if user can share today (rate limiting) /// Returns true if user has not exceeded daily limit Future canShareToday(String telegramUserId, int dailyLimit) async { try { final response = await _request( method: 'POST', path: '/telegram-bot/share/check-limit', body: { 'telegramUserId': telegramUserId, 'dailyLimit': dailyLimit, }, ); return response['canShare'] as bool? ?? true; } catch (e, s) { print('Error checking share limit: $e\n$s'); return true; // Allow on error to prevent blocking users } } /// Record a share request for a user Future recordShareRequest({ required String telegramUserId, String? telegramUsername, String? sharedCardId, }) async { try { await _request( method: 'POST', path: '/telegram-bot/share/record', body: { 'telegramUserId': telegramUserId, if (telegramUsername != null) 'telegramUsername': telegramUsername, if (sharedCardId != null) 'sharedCardId': sharedCardId, }, ); print( '[SHARE_RECORDED] User $telegramUserId shared card $sharedCardId'); } catch (e, s) { print('Error recording share request: $e\n$s'); } } /// Get a random card from the database /// Returns null if no cards are found Future getRandomCard() async { try { final response = await _request( method: 'GET', path: '/telegram-bot/random-card', ); if (response.isEmpty) { return null; } // Convert response to GameCardModel // Note: GameCardModel requires non-nullable fields, so we provide defaults return GameCardModel( id: response['id'] as String, original: response['original'] as String? ?? '', translation: response['translation'] as String? ?? '', mnemo: response['mnemo'] as String? ?? '', image: response['image'] as String? ?? '', transcription: response['transcription'] as String?, transcriptionMnemo: response['transcriptionMnemo'] as String?, imageBack: response['imageBack'] as String?, back: response['back'] as String?, ); } catch (e, s) { print('Error getting random card: $e\n$s'); return null; } } /// Get user information Future userInfo(String userId) async { try { final response = await _request( method: 'GET', path: '/telegram-bot/users/info?userId=$userId', ); if (response['multiple'] == true) { final users = response['users'] as List; return 'Found multiple:\n${users.map((u) => '${u['id']} ${u['email'] ?? ''} ${u['name'] ?? ''}').join('\n')}'; } final sub = response['subscription'] as Map?; final packs = response['packs'] as List?; return ''' ${response['id']} ${response['email'] ?? ''} ${response['name'] ?? ''} Tags: ${response['tags'] ?? ''} Packs (${packs?.length ?? 0}): ${packs?.map((p) => '${p['id']} ${p['title']}').join('\n') ?? ''} Purchases: ${response['purchases'] ?? 0} Subscription ${sub == null ? '' : '(${sub['start']} - ${sub['finish']})'}: ${sub?['features']?.join(' ') ?? ''} ''' .trim(); } catch (e, s) { print('Error getting user info: $e\n$s'); return 'Error: $e'; } } /// Get all users summary Future info() async { try { final response = await _request( method: 'GET', path: '/telegram-bot/users/info', ); final users = response['users'] as List; return 'Users (${response['total']}):\n' '${users.map((u) => '${u['email'] ?? '${u['id']}'} ${u['lastTimeOnline'] ?? ''}').join('\n')}'; } catch (e, s) { print('Error getting users info: $e\n$s'); return 'Error: $e'; } } /// Get all words from cards Future> words() async { try { final response = await _request( method: 'GET', path: '/telegram-bot/words', ); final wordsList = response['words'] as List?; return wordsList?.map((w) => w.toString()).toList() ?? []; } catch (e, s) { print('Error getting words: $e\n$s'); return []; } } void dispose() { _httpClient.close(force: true); } }