import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:drift/drift.dart'; import 'package:firebase_core/firebase_core.dart'; import '../../database/database.dart'; import '../auth/auth_service.dart'; import 'cloud_models.dart'; /// Synchronizes account/profile and motorcycle metadata independently of the /// ride upload pipeline. This keeps identity data small and queryable while /// leaving high-frequency telemetry in the existing ride/R2 path. class FirebaseAccountSyncService { final AppDatabase db; final AuthService auth; final FirebaseFirestore? _firestore; FirebaseAccountSyncService({ required this.db, required this.auth, FirebaseFirestore? firestore, }) : _firestore = firestore ?? _resolveFirestore(); static FirebaseFirestore? _resolveFirestore() { try { if (Firebase.apps.isNotEmpty) return FirebaseFirestore.instance; } catch (_) {} return null; } bool get isAvailable => _firestore != null && Firebase.apps.isNotEmpty; DateTime? _parseDateTime(dynamic value) { if (value == null) return null; if (value is Timestamp) return value.toDate(); if (value is String) return DateTime.tryParse(value); if (value is int) return DateTime.fromMillisecondsSinceEpoch(value); return null; } Future syncAccount({DateTime? now}) async { final user = auth.currentUser; if (user == null || user.isAnonymous || !isAvailable) return; final timestamp = (now ?? DateTime.now()).toUtc(); final profile = CloudUserProfile( id: user.uid, displayName: user.displayName, profilePhotoUrl: user.photoUrl, createdAt: user.createdAt?.toUtc() ?? timestamp, updatedAt: timestamp, ); final userRef = _firestore!.collection('users').doc(user.uid); await userRef.set(profile.toJson(), SetOptions(merge: true)); // 1. Fetch remote motorcycles from Firestore and reconcile into local Drift DB try { final remoteSnapshot = await userRef.collection('motorcycles').get(); final localBikes = await db.select(db.garageMotorcycles).get(); final localById = {for (final b in localBikes) b.id.toString(): b}; final localByKey = {for (final b in localBikes) '${b.year}_${b.makeName}_${b.modelName}'.toLowerCase(): b}; for (final doc in remoteSnapshot.docs) { final data = doc.data(); final docId = doc.id; final year = (data['year'] as num?)?.toInt() ?? 2000; final makeName = (data['makeName'] ?? '').toString(); final modelName = (data['modelName'] ?? '').toString(); final key = '${year}_${makeName}_$modelName'.toLowerCase(); final existing = localById[docId] ?? localByKey[key]; if (existing == null) { final parsedId = int.tryParse(docId); await db.into(db.garageMotorcycles).insert( GarageMotorcyclesCompanion( id: parsedId != null ? Value(parsedId) : const Value.absent(), year: Value(year), makeId: Value((data['makeId'] ?? parsedId?.toString() ?? '1').toString()), makeName: Value(makeName), modelId: Value((data['modelId'] ?? parsedId?.toString() ?? '1').toString()), modelName: Value(modelName), variantId: Value(data['variantId']?.toString()), variantName: Value(data['variantName']?.toString()), nickname: Value(data['nickname']?.toString()), isDefault: Value(data['isDefault'] == true), dateAdded: Value(_parseDateTime(data['createdAt']) ?? timestamp), lastUpdated: Value(_parseDateTime(data['updatedAt']) ?? timestamp), specificationJson: Value(data['specificationJson']?.toString()), ), mode: InsertMode.insertOrReplace, ); } else if (existing.specificationJson == null && data['specificationJson'] != null) { await (db.update(db.garageMotorcycles)..where((t) => t.id.equals(existing.id))) .write(GarageMotorcyclesCompanion( specificationJson: Value(data['specificationJson']?.toString()), lastUpdated: Value(timestamp), )); } } } catch (_) { // Remote fetch error should not block local operations } // 2. Upload any local motorcycles to Firestore final currentLocal = await db.select(db.garageMotorcycles).get(); for (final motorcycle in currentLocal) { final cloud = CloudMotorcycle( id: motorcycle.id.toString(), ownerId: user.uid, year: motorcycle.year, makeId: motorcycle.makeId, makeName: motorcycle.makeName, modelId: motorcycle.modelId, modelName: motorcycle.modelName, variantId: motorcycle.variantId, variantName: motorcycle.variantName, nickname: motorcycle.nickname, isDefault: motorcycle.isDefault, createdAt: motorcycle.dateAdded.toUtc(), updatedAt: motorcycle.lastUpdated.toUtc(), specificationJson: motorcycle.specificationJson, ); await userRef.collection('motorcycles').doc(cloud.id).set( cloud.toJson(), SetOptions(merge: true), ); } // 3. Fallback synthesis: If garage is empty but restored rides reference a motorcycle if (currentLocal.isEmpty) { await _synthesizeMotorcycleFromRidesIfEmpty(user.uid, userRef, timestamp); } } Future _synthesizeMotorcycleFromRidesIfEmpty( String uid, DocumentReference userRef, DateTime timestamp, ) async { try { final ridesWithBikes = await (db.select(db.rides) ..where((r) => r.motorcycleMake.isNotNull() & r.motorcycleModel.isNotNull()) ..orderBy([(r) => OrderingTerm(expression: r.startTime, mode: OrderingMode.desc)]) ..limit(1)) .get(); if (ridesWithBikes.isNotEmpty) { final r = ridesWithBikes.first; final bikeId = r.motorcycleId ?? 1; final year = r.motorcycleYear ?? 2001; final make = r.motorcycleMake ?? 'Unknown'; final model = r.motorcycleModel ?? 'Unknown'; final variant = r.motorcycleVariant; await db.into(db.garageMotorcycles).insert( GarageMotorcyclesCompanion( id: Value(bikeId), year: Value(year), makeId: Value(bikeId.toString()), makeName: Value(make), modelId: Value(bikeId.toString()), modelName: Value(model), variantName: Value(variant), isDefault: const Value(true), dateAdded: Value(timestamp), lastUpdated: Value(timestamp), ), mode: InsertMode.insertOrReplace, ); final cloud = CloudMotorcycle( id: bikeId.toString(), ownerId: uid, year: year, makeId: bikeId.toString(), makeName: make, modelId: bikeId.toString(), modelName: model, variantName: variant, isDefault: true, createdAt: timestamp, updatedAt: timestamp, ); await userRef.collection('motorcycles').doc(cloud.id).set( cloud.toJson(), SetOptions(merge: true), ); } } catch (_) {} } }