import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'dart:io'; import 'package:http/http.dart' as http; import 'package:device_info_plus/device_info_plus.dart'; import 'package:crypto/crypto.dart'; import '../data/models/shot.dart'; import '../data/models/target_type.dart'; import 'wallet_identity_service.dart'; /// Résultat détaillé de l'exportation vers le serveur IA class AiExportResult { final bool isSuccess; final String code; final String message; final String? reason; final bool isBanned; final Map? targetValidation; AiExportResult({ required this.isSuccess, required this.code, required this.message, this.reason, this.isBanned = false, this.targetValidation, }); factory AiExportResult.success({ String? message, Map? targetValidation, }) { return AiExportResult( isSuccess: true, code: 'UPLOAD_SUCCESS', message: message ?? 'Export réussi vers le serveur IA !', targetValidation: targetValidation, ); } factory AiExportResult.banned({ String? reason, String? message, }) { return AiExportResult( isSuccess: false, code: 'WALLET_BANNED', isBanned: true, reason: reason, message: message ?? 'Votre participation au programme d\'entraînement IA a été suspendue par la modération.', ); } factory AiExportResult.error({ String? code, required String message, }) { return AiExportResult( isSuccess: false, code: code ?? 'UPLOAD_ERROR', message: message, ); } } class AiExportService { /// Extrait les informations de l'appareil Future> _getDeviceInfo() async { final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); Map deviceData = {'model': 'Unknown', 'os': 'Unknown'}; try { if (Platform.isAndroid) { final androidInfo = await deviceInfoPlugin.androidInfo; deviceData['model'] = '${androidInfo.brand} ${androidInfo.model}'; deviceData['os'] = 'Android ${androidInfo.version.release}'; } else if (Platform.isIOS) { final iosInfo = await deviceInfoPlugin.iosInfo; deviceData['model'] = iosInfo.name; deviceData['os'] = '${iosInfo.systemName} ${iosInfo.systemVersion}'; } else if (Platform.isWindows) { final windowsInfo = await deviceInfoPlugin.windowsInfo; deviceData['model'] = 'Windows PC'; deviceData['os'] = 'Windows ${windowsInfo.majorVersion}.${windowsInfo.minorVersion}'; } } catch (e) { debugPrint('Erreur lors de la récupération des infos appareil: $e'); } return deviceData; } /// Exporte l'image et les données de plotting vers le serveur Future exportData({ required String imagePath, required String sessionId, required TargetType targetType, required double targetCenterX, required double targetCenterY, required double targetRadius, required List shots, int distanceMeters = 25, String weaponName = 'Unknown', String? apiUrl, }) async { try { final walletService = WalletIdentityService(); final rawBaseUrl = await walletService.getServerBaseUrl(); final baseUrl = rawBaseUrl.endsWith('/') ? rawBaseUrl.substring(0, rawBaseUrl.length - 1) : rawBaseUrl; final effectiveUrl = apiUrl ?? '$baseUrl/api/upload'; final url = Uri.parse(effectiveUrl); final request = http.MultipartRequest('POST', url); request.headers['X-API-KEY'] = WalletIdentityService.apiKey; // 1. Prepare image final file = File(imagePath); if (!await file.exists()) { return AiExportResult.error( code: 'FILE_NOT_FOUND', message: 'Le fichier image cible est introuvable.', ); } final deviceData = await _getDeviceInfo(); final corners = [ {"norm_x": targetCenterX - targetRadius, "norm_y": targetCenterY - targetRadius}, {"norm_x": targetCenterX + targetRadius, "norm_y": targetCenterY - targetRadius}, {"norm_x": targetCenterX + targetRadius, "norm_y": targetCenterY + targetRadius}, {"norm_x": targetCenterX - targetRadius, "norm_y": targetCenterY + targetRadius}, ]; // Format the impacts final formattedImpacts = shots.asMap().entries.map((entry) { final index = entry.key; final shot = entry.value; return { "id": index + 1, "label": "bullet_hole", "score": shot.score, "coords": { "norm_x": shot.x, "norm_y": shot.y } }; }).toList(); // Get and hash the wallet identity final phrase = await walletService.getIdentityPhrase(); final phraseBytes = utf8.encode(phrase); final walletHash = sha256.convert(phraseBytes).toString(); // Build JSON payload final plottingJson = { "session_id": sessionId, "wallet_hash": walletHash, "timestamp": DateTime.now().toIso8601String(), "device_info": deviceData, "target_metadata": { "type": targetType.name, "distance_meters": distanceMeters, "weapon": weaponName, }, "plotting": { "target_corners": corners, "impacts": formattedImpacts } }; request.fields['plotting'] = jsonEncode(plottingJson); request.files.add( await http.MultipartFile.fromPath('photo', imagePath), ); final streamedResponse = await request.send().timeout( const Duration(seconds: 15), onTimeout: () => throw Exception('Délai d\'attente dépassé (timeout)'), ); final responseBody = await streamedResponse.stream.bytesToString(); Map responseJson = {}; try { responseJson = jsonDecode(responseBody); } catch (_) {} final statusCode = streamedResponse.statusCode; if (statusCode == 200) { debugPrint('Export réussi: $responseBody'); return AiExportResult.success( message: responseJson['message'] ?? 'Photo et données exportées avec succès.', targetValidation: responseJson['target_validation'] as Map?, ); } else if (statusCode == 403 || responseJson['code'] == 'WALLET_BANNED') { final reason = responseJson['reason'] ?? 'Non-respect des règles de contribution'; debugPrint('Export rejeté (banni): $reason'); // Persister le bannissement localement et couper l'envoi de photos await walletService.setBanned(true, reason: reason); return AiExportResult.banned( reason: reason, message: responseJson['error'] ?? 'Votre wallet a été suspendu par la modération.', ); } else if (statusCode == 400) { return AiExportResult.error( code: responseJson['code'] ?? 'BAD_REQUEST', message: responseJson['error'] ?? 'Requête d\'export invalide.', ); } else { return AiExportResult.error( code: responseJson['code'] ?? 'SERVER_ERROR', message: responseJson['error'] ?? 'Erreur serveur ($statusCode).', ); } } on SocketException { return AiExportResult.error( code: 'NETWORK_ERROR', message: 'Impossible de joindre le serveur IA. Vérifiez l\'adresse IP ou votre connexion.', ); } catch (e) { debugPrint('Exception lors de l\'export: $e'); return AiExportResult.error( code: 'UNKNOWN_ERROR', message: 'Erreur lors de l\'export: $e', ); } } }