import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/config.dart'; import '../providers/auth_provider.dart'; import 'models/invite_preview.dart'; import 'models/match.dart'; import 'models/recording_item.dart'; import 'models/score_state.dart'; import 'models/stream_session.dart'; import 'models/team.dart'; import 'models/user.dart'; final apiClientProvider = Provider((ref) { final auth = ref.watch(authProvider); return ApiClient(accessToken: auth.accessToken); }); class ApiClient { ApiClient({this.accessToken}) { _dio = Dio( BaseOptions( baseUrl: AppConfig.apiV1, connectTimeout: const Duration(seconds: 30), receiveTimeout: const Duration(seconds: 30), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, ), ); _dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) { if (accessToken != null && accessToken!.isNotEmpty) { options.headers['Authorization'] = 'Bearer $accessToken'; } handler.next(options); }, ), ); } late final Dio _dio; final String? accessToken; // --- Auth --- Future<({User user, AuthTokens tokens})> login({ required String email, required String password, }) async { final response = await _dio.post>( '/auth/login', data: {'email': email, 'password': password}, ); final data = response.data!; return ( user: User.fromJson(data['user'] as Map), tokens: AuthTokens.fromJson(data), ); } Future<({User user, AuthTokens tokens})> refreshSession( String refreshToken, ) async { final response = await _dio.post>( '/auth/refresh', data: {'refresh_token': refreshToken}, ); final data = response.data!; return ( user: User.fromJson(data['user'] as Map), tokens: AuthTokens.fromJson(data), ); } Future me() async { final response = await _dio.get>('/auth/me'); return User.fromJson(response.data!); } Future logout() async { await _dio.post('/auth/logout'); } // --- Teams --- Future> fetchTeams() async { final response = await _dio.get>('/teams'); return response.data! .map((e) => Team.fromJson(e as Map)) .toList(); } Future fetchInvitePreview(String token) async { final response = await _dio.get>('/invitations/$token'); return InvitePreview.fromJson(response.data!); } Future acceptInvitation(String token) async { final response = await _dio.post>('/invitations/$token/accept'); return response.data!['message'] as String? ?? 'Invito accettato'; } Future youtubeAuthorizeUrl(String teamId, {bool returnApp = true}) async { final response = await _dio.get>( '/teams/$teamId/youtube/authorize', queryParameters: returnApp ? {'return_app': '1'} : null, ); return response.data!['authorization_url'] as String; } Future> fetchRecordings(String teamId) async { final response = await _dio.get>('/teams/$teamId/recordings'); return response.data! .map((e) => RecordingItem.fromJson(e as Map)) .toList(); } Future fetchRecordingDownloadUrl(String recordingId) async { final response = await _dio.get>( '/recordings/$recordingId/download', ); return response.data!['download_url'] as String; } // --- Matches --- Future> fetchMatches(String teamId) async { final response = await _dio.get>('/teams/$teamId/matches'); return response.data! .map((e) => MatchModel.fromJson(e as Map)) .toList(); } Future fetchMatch(String matchId) async { final response = await _dio.get>('/matches/$matchId'); return MatchModel.fromJson(response.data!); } Future updateMatch(String matchId, Map body) async { final response = await _dio.patch>( '/matches/$matchId', data: {'match': body}, ); return MatchModel.fromJson(response.data!); } Future deleteMatch(String matchId) async { await _dio.delete('/matches/$matchId'); } Future createMatch(String teamId, Map body) async { final response = await _dio.post>( '/teams/$teamId/matches', data: {'match': body}, ); return MatchModel.fromJson(response.data!); } // --- Sessions --- Future createSession( String matchId, { String platform = 'matchlivetv', String? youtubeChannel, String privacyStatus = 'public', String qualityPreset = '720p_30_2.5mbps', int? targetBitrate, int? targetFps, }) async { // YouTube: creazione broadcast + path MediaMTX può richiedere 20–30s lato server. final sessionTimeout = platform == 'youtube' ? const Duration(seconds: 60) : const Duration(seconds: 30); final response = await _dio.post>( '/matches/$matchId/sessions', data: { 'platform': platform, 'privacy_status': privacyStatus, 'quality_preset': qualityPreset, if (youtubeChannel != null) 'youtube_channel': youtubeChannel, if (targetBitrate != null) 'target_bitrate': targetBitrate, if (targetFps != null) 'target_fps': targetFps, }, options: Options( sendTimeout: sessionTimeout, receiveTimeout: sessionTimeout, ), ); return StreamSession.fromJson(response.data!); } Future fetchSession(String sessionId) async { final response = await _dio.get>('/sessions/$sessionId'); return StreamSession.fromJson(response.data!); } Future startSession(String sessionId) async { final response = await _dio.patch>('/sessions/$sessionId/start'); return StreamSession.fromJson(response.data!); } Future stopSession(String sessionId) async { final response = await _dio.patch>('/sessions/$sessionId/stop'); return StreamSession.fromJson(response.data!); } Future pauseSession(String sessionId) async { final response = await _dio.patch>('/sessions/$sessionId/pause'); return StreamSession.fromJson(response.data!); } Future resumeSession(String sessionId) async { final response = await _dio.patch>('/sessions/$sessionId/resume'); return StreamSession.fromJson(response.data!); } /// Persiste il tabellone e restituisce lo stato salvato sul server. Future syncScore(String sessionId, Map payload) async { final response = await _dio.patch>( '/sessions/$sessionId/score', data: payload, ); final score = response.data?['score']; if (score is Map) { return ScoreState.fromJson(score); } return null; } Future createRegiaLink(String sessionId) async { final response = await _dio.post>( '/sessions/$sessionId/regia_link', ); return RegiaLinkResponse.fromJson(response.data!); } Future createPairingToken(String sessionId) async { final response = await _dio.post>( '/sessions/$sessionId/pairing_token', ); return PairingTokenResponse.fromJson(response.data!); } Future claimPairing({ required String sessionId, required String pairingToken, required String deviceRole, }) async { final response = await _dio.post>( '/sessions/$sessionId/claim_pairing', data: { 'pairing_token': pairingToken, 'device_role': deviceRole, }, ); return StreamSession.fromJson(response.data!); } Future networkTest({ required String sessionId, required double downloadMbps, required double uploadMbps, required int latencyMs, required String networkType, }) async { final response = await _dio.post>( '/sessions/$sessionId/network_test', data: { 'download_mbps': downloadMbps, 'upload_mbps': uploadMbps, 'latency_ms': latencyMs, 'network_type': networkType, }, ); return NetworkTestResult.fromJson(response.data!); } Future youtubeStats(String sessionId) async { final response = await _dio.get>('/sessions/$sessionId/youtube_stats'); return YoutubeStats.fromJson(response.data!); } Future postTelemetry({ required String sessionId, required String deviceRole, int? batteryLevel, String? networkType, int? signalStrength, int? currentBitrate, int? targetBitrate, int? fps, }) async { await _dio.post( '/sessions/$sessionId/telemetry', data: { 'device_role': deviceRole, if (batteryLevel != null) 'battery_level': batteryLevel, if (networkType != null) 'network_type': networkType, if (signalStrength != null) 'signal_strength': signalStrength, if (currentBitrate != null) 'current_bitrate': currentBitrate, if (targetBitrate != null) 'target_bitrate': targetBitrate, if (fps != null) 'fps': fps, }, ); } }