import 'dart:convert'; import 'dart:io'; import 'package:archive/archive.dart'; import 'package:drift/drift.dart' as drift; import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import '../../core/version/app_identity.dart'; import '../../database/database.dart'; import '../export_service.dart'; import '../field_test/field_test_export_service.dart'; /// Top-level isolate entrypoint for CPU-heavy ZIP compression. /// Never receives non-isolate-safe objects (database handles, streams, etc.). Uint8List compressFilesToZipIsolate(Map filesMap) { final archive = Archive(); for (final entry in filesMap.entries) { final bytes = utf8.encode(entry.value); archive.addFile(ArchiveFile(entry.key, bytes.length, bytes)); } final zipEncoder = ZipEncoder(); // Keep retry bytes and the recorded SHA-256 stable for unchanged ride data. final compressed = zipEncoder.encode(archive, modified: DateTime.utc(1980, 1, 1)); return Uint8List.fromList(compressed); } /// Service that queries local forensic datasets and offloads ZIP compression /// to a background worker isolate via compute(), preventing main thread stutter. class SyncBundleWorker { final AppDatabase _db; SyncBundleWorker(this._db); /// Generates the standard multi-format forensic bundle for a ride session /// and compresses it inside a background isolate. Future generateCompressedBundle(int rideId) async { try { final ride = await (_db.select(_db.rides)..where((r) => r.id.equals(rideId))).getSingleOrNull(); if (ride == null) return null; final fieldTestExport = FieldTestExportService(_db); final exportService = ExportService(_db); // 1. Query local authoritative data on the main isolate final sessionJson = await fieldTestExport.generateFieldTestSessionJson( rideId, generatedAt: ride.endTime ?? ride.startTime, ); final eventsCsv = await fieldTestExport.generateFieldTestEventsCsv(rideId); final telemetryCsv = await exportService.exportRideCsv(rideId); final gpxContent = await exportService.exportRideGpx(rideId); final samples = await (_db.select(_db.telemetrySamples) ..where((t) => t.rideId.equals(rideId)) ..orderBy([(t) => drift.OrderingTerm(expression: t.timestamp)]) ).get(); final events = await (_db.select(_db.fieldTestEvents) ..where((e) => e.rideId.equals(rideId)) ..orderBy([(e) => drift.OrderingTerm(expression: e.timestamp)]) ).get(); final summaryRecord = await (_db.select(_db.fieldTestSummaries) ..where((s) => s.rideId.equals(rideId)) ).getSingleOrNull(); final appIdentity = AppIdentity.current(); // 2. Build README.txt audit header final readme = StringBuffer(); readme.writeln('====================================================='); readme.writeln('MOTORCYCLE DASH — FORENSIC FIELD TEST SESSION REPORT'); readme.writeln('====================================================='); readme.writeln('Application Version: ${appIdentity.semanticVersion}'); readme.writeln('Build Number: ${appIdentity.buildNumber}'); readme.writeln('Git Revision: ${appIdentity.gitCommit}'); readme.writeln('Git Branch: ${appIdentity.gitBranch}'); readme.writeln('Release Channel: ${appIdentity.releaseChannel}'); readme.writeln('Telemetry Schema Version: ${appIdentity.telemetrySchemaVersion}'); readme.writeln('Export Format Version: ${appIdentity.exportSchemaVersion}'); readme.writeln('Field Test Session / Ride ID: $rideId'); readme.writeln('Session Start: ${ride.startTime.toIso8601String()}'); readme.writeln('Session End: ${ride.endTime?.toIso8601String() ?? "IN_PROGRESS"}'); readme.writeln('Summary Rating: ${summaryRecord?.rideQualityRating ?? "COMPLETED"}'); readme.writeln('Ride Quality Score: ${summaryRecord?.rideQualityScore ?? 100} / 100'); readme.writeln('Total Telemetry Samples: ${samples.length}'); readme.writeln('Total Forensic Events: ${events.length}'); // 3. Build ride_summary.json final rideSummaryJson = const JsonEncoder.withIndent(' ').convert({ 'rideId': ride.id, 'startTime': ride.startTime.toIso8601String(), 'endTime': ride.endTime?.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, 'qualityScore': summaryRecord?.rideQualityScore ?? 100, 'qualityRating': summaryRecord?.rideQualityRating ?? 'EXCELLENT', 'motorcycleId': ride.motorcycleId, 'motorcycleMake': ride.motorcycleMake, 'motorcycleModel': ride.motorcycleModel, 'motorcycleYear': ride.motorcycleYear, 'software': appIdentity.toJson(), }); // 4. Assemble isolated serializable string DTO map final filesMap = { 'README.txt': readme.toString(), 'ride_summary.json': rideSummaryJson, 'field_test_session.json': sessionJson, 'field_test_events.csv': eventsCsv, 'telemetry.csv': telemetryCsv, }; if (gpxContent.isNotEmpty) { filesMap['telemetry.gpx'] = gpxContent; } // 5. Offload CPU-heavy compression to background worker isolate return await compute(compressFilesToZipIsolate, filesMap); } catch (e) { debugPrint('[SyncBundleWorker] Bundle generation error: $e'); return null; } } /// Writes compressed bytes to a temporary file for local inspection or upload Future writeBundleToTempFile(int rideId, Uint8List zipBytes) async { try { final tempDir = await getTemporaryDirectory(); final dateStr = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); final file = File('${tempDir.path}/ride_${rideId}_$dateStr.zip'); return await file.writeAsBytes(zipBytes); } catch (e) { debugPrint('[SyncBundleWorker] Error writing temp zip file: $e'); return null; } } }