import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:crypto/crypto.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:intl/intl.dart'; import '../../core/version/app_identity.dart'; import '../../database/database.dart'; import '../auth/auth_service.dart'; import 'r2_upload_service.dart'; import 'sync_bundle_worker.dart'; import 'sync_models.dart'; import 'sync_provider.dart'; /// Authoritative Firebase + Cloudflare R2 Synchronization Provider. /// Synchronizes structured ride metadata to Cloud Firestore and streams /// compressed 20Hz forensic bundles to Cloudflare R2 via the authenticated Worker gateway. class FirebaseSyncProvider implements SyncProvider { final AppDatabase _db; final AuthService _authService; final FirebaseFirestore? _firestore; final R2UploadService _r2UploadService; final SyncBundleWorker _bundleWorker; FirebaseSyncProvider({ required AppDatabase db, required AuthService authService, FirebaseFirestore? firestore, R2UploadService? r2UploadService, SyncBundleWorker? bundleWorker, }) : _db = db, // ignore: prefer_initializing_formals _authService = authService, _firestore = firestore ?? _resolveFirestore(), _r2UploadService = r2UploadService ?? R2UploadService(), _bundleWorker = bundleWorker ?? SyncBundleWorker(db); static FirebaseFirestore? _resolveFirestore() { try { if (Firebase.apps.isNotEmpty) { return FirebaseFirestore.instance; } } catch (_) {} return null; } @override String get providerId => 'firebase'; @override Future isAvailable() async { try { if (Firebase.apps.isEmpty) return false; if (_firestore == null) return false; return true; } catch (_) { return false; } } @override Future requiresAuthentication() async { final user = _authService.currentUser; if (user == null) return true; return false; } @override Future uploadRide(SyncItem item) async { final available = await isAvailable(); if (!available) { return SyncResult.temporaryFailure( 'Firebase services are unavailable or unconfigured. Ride remains safely stored locally.', ); } final user = _authService.currentUser; if (user == null) { return SyncResult.authRequired( message: 'Sign in to MotorCloud is required for cloud backup.', ); } final idToken = await _authService.getIdToken(); if (idToken == null || idToken.isEmpty) { return SyncResult.authRequired( message: 'Valid Firebase authentication token required for cloud sync.', ); } try { // 1. Read authoritative local ride record final ride = await (_db.select(_db.rides)..where((r) => r.id.equals(item.localRideId))).getSingleOrNull(); if (ride == null) { return SyncResult.permanentFailure( 'Local ride #${item.localRideId} not found in database.', ); } // 2. Generate deterministic remote document and bundle identifiers final dateStr = DateFormat('yyyyMMdd_HHmmss').format(ride.startTime.toUtc()); final remoteRideId = 'ride_${item.localRideId}_$dateStr'; final appIdentity = AppIdentity.current(); // 3. Generate compressed forensic bundle in background worker isolate final zipBytes = await _bundleWorker.generateCompressedBundle(item.localRideId); if (zipBytes == null || zipBytes.isEmpty) { return SyncResult.permanentFailure( 'Failed generating compressed telemetry bundle for ride #${item.localRideId}.', ); } final sha256Hash = sha256.convert(zipBytes).toString(); // 4. Stream binary telemetry.zip to Cloudflare R2 via secure Worker gateway with verified Firebase ID token final r2Result = await _r2UploadService.uploadTelemetryBundle( idToken: idToken, remoteRideId: remoteRideId, zipBytes: zipBytes, sha256Hash: sha256Hash, appVersion: appIdentity.semanticVersion, ); if (!r2Result.success) { if (r2Result.requiresAuth) { return SyncResult.authRequired(message: r2Result.errorMessage ?? 'R2 Gateway authentication required'); } if (r2Result.isRetryable) { return SyncResult.temporaryFailure(r2Result.errorMessage ?? 'Temporary R2 upload error'); } return SyncResult.permanentFailure(r2Result.errorMessage ?? 'Permanent R2 upload rejection'); } final objectKey = r2Result.objectKey ?? 'users/${user.uid}/rides/$remoteRideId/telemetry.zip'; // 5. Query discrete events and performance benchmarks for rich metadata final events = await (_db.select(_db.rideEvents)..where((e) => e.rideId.equals(item.localRideId))).get(); final performanceRuns = await (_db.select(_db.performanceRuns)..where((p) => p.rideId.equals(item.localRideId))).get(); final summary = await (_db.select(_db.fieldTestSummaries)..where((s) => s.rideId.equals(item.localRideId))).getSingleOrNull(); // 6. Build queryable Firestore metadata document referencing Cloudflare R2 final firestorePayload = { 'localRideId': ride.id, 'remoteRideId': remoteRideId, 'uid': user.uid, 'startTime': ride.startTime.toUtc().toIso8601String(), 'endTime': ride.endTime?.toUtc().toIso8601String(), 'distanceMiles': ride.distanceMiles, 'topSpeedMph': ride.topSpeedMph, 'maxLeanLeft': ride.maxLeanLeft, 'maxLeanRight': ride.maxLeanRight, 'maxAccelG': ride.maxAccelG, 'maxBrakingG': ride.maxBrakingG, 'averageSpeed': ride.averageSpeed, 'movingTime': ride.movingTime, 'stoppedTime': ride.stoppedTime, 'elevationGain': ride.elevationGain, 'elevationLoss': ride.elevationLoss, 'qualityScore': summary?.rideQualityScore ?? 100, 'qualityRating': summary?.rideQualityRating ?? 'EXCELLENT', 'motorcycleId': ride.motorcycleId, 'motorcycleMake': ride.motorcycleMake, 'motorcycleModel': ride.motorcycleModel, 'motorcycleYear': ride.motorcycleYear, 'motorcycleVariant': ride.motorcycleVariant, 'hardBrakingEvents': events.where((e) => e.eventType.contains('Braking')).length, 'extremeLeanEvents': events.where((e) => e.eventType.contains('Lean') || e.eventType.contains('Corner')).length, 'highSpeedEvents': events.where((e) => e.eventType.contains('Speed')).length, 'totalEventsCount': events.length, 'performanceRuns': performanceRuns.map((r) => { 'type': r.type, 'timeMs': r.timeMs, 'topSpeedReached': r.topSpeedReached, 'timestamp': r.timestamp.toUtc().toIso8601String(), }).toList(), 'telemetryBundle': { 'provider': 'cloudflare-r2', 'objectKey': objectKey, 'status': 'uploaded', 'bundleSizeBytes': zipBytes.length, 'sha256': sha256Hash, }, 'telemetryBundlePath': objectKey, 'bundleSizeBytes': zipBytes.length, 'syncVersion': 2, 'uploadedAt': FieldValue.serverTimestamp(), 'software': appIdentity.toJson(), }; // 7. Write / Merge Firestore summary document idempotently await _firestore! .collection('users') .doc(user.uid) .collection('rides') .doc(remoteRideId) .set(firestorePayload, SetOptions(merge: true)); return SyncResult.success(remoteId: remoteRideId); } on FirebaseException catch (e) { if (e.code == 'permission-denied' || e.code == 'unauthenticated') { return SyncResult.authRequired( message: 'Firebase authorization error: ${e.message}', ); } if (e.code == 'unavailable' || e.code == 'network-request-failed' || e.code == 'retry-limit-exceeded') { return SyncResult.temporaryFailure( 'Network or Firebase service temporary error (${e.code}): ${e.message}', ); } return SyncResult.permanentFailure( 'Firebase sync error (${e.code}): ${e.message}', ); } catch (e) { return SyncResult.temporaryFailure('Unexpected sync exception: $e'); } } }