import 'dart:convert'; import '../modelos/emisora.dart'; import '../modelos/grupo_favoritos.dart'; import '../modelos/preset_ecualizador.dart'; /// Owns the backup (export/import) JSON serialization (S4-R4). /// /// v3 extends v2 with `presetsPorDispositivo`, `presetsMatriz`, and /// `eqMultiDeviceEnabled`. v4 extends v3 with `ecualizadorActivo` (the /// equalizer's global ON/OFF toggle). When the version-N extension /// parameters are all omitted the export stays at the lower version for /// backward compat with older app builds. State APPLICATION (writing /// favorites, EQ, alarms back into the app) stays in /// `EstadoRadio.importarConfig` — this service only owns serialization, /// parsing and the envelope shape. class ServicioExportImport { const ServicioExportImport(); /// Current backup schema version (v4 — equalizer on/off toggle). static const int versionActual = 4; /// v3 version constant (multi-device EQ) kept for clarity. static const int versionV3 = 3; /// Legacy v2 version constant kept for clarity. static const int versionV2 = 2; /// Builds the export envelope. /// /// When [presetsPorDispositivo] or [presetsMatriz] or /// [eqMultiDeviceEnabled] are provided (non-null), at least [versionV3] (3) /// is written. When [ecualizadorActivo] is ALSO provided (non-null), /// [versionActual] (4) is written. Omitting all of them behaves identically /// to the original v2 format (version key stays 2) so old backups keep /// round-tripping without version bumps. /// /// The `alarmas` block is the RAW JSON map persisted by ServicioAlarmas /// and passes through untouched (no re-parsing here). Map construirExportacion({ required List gruposFavoritos, required List favoritos, required List emisorasCustom, required PresetEcualizador presetPrincipal, required Map presetsPorEmisora, required Map? alarmas, required String? emisoraPreferidaUuid, required String ordenListas, required List timerSuenoPresetsSegundos, DateTime? exportadoEn, // v3 extensions — omitting these produces a v2-compatible export. Map? presetsPorDispositivo, Map? presetsMatriz, bool? eqMultiDeviceEnabled, // v4 extension — the equalizer's global ON/OFF toggle. Omitting it // produces a v3 (or v2)-compatible export. bool? ecualizadorActivo, }) { final tieneExtensionesV3 = presetsPorDispositivo != null || presetsMatriz != null || eqMultiDeviceEnabled != null; final tieneExtensionV4 = ecualizadorActivo != null; final int version; if (tieneExtensionV4) { version = versionActual; } else if (tieneExtensionesV3) { version = versionV3; } else { version = versionV2; } final envelope = { 'version': version, 'exportedAt': (exportadoEn ?? DateTime.now()).toIso8601String(), // Favorites + groups (preserves grupo_id assignments per station). // The protected "sin asignar" group is implicit and never exported. 'gruposFavoritos': gruposFavoritos .where((g) => !g.esSinAsignar) .map((g) => g.toMap()) .toList(), 'favoritos': favoritos.map((e) => e.toMap()).toList(), // Custom stations. 'emisorasCustom': emisorasCustom.map((e) => e.toMap()).toList(), // Equalizer (base fields — present in all versions). 'presetPrincipalEcualizador': presetPrincipal.toJson(), 'presetsEcualizador': presetsPorEmisora.map( (uuid, preset) => MapEntry(uuid, preset.toJson()), ), // Full alarm block (alarms + vacations + exceptions) — raw passthrough. 'alarmas': alarmas, // User preferences. 'emisoraPreferidaUuid': emisoraPreferidaUuid, 'ordenListas': ordenListas, 'timerSuenoPresetsSegundos': timerSuenoPresetsSegundos, }; // v3 extensions: only written when explicitly provided. if (tieneExtensionesV3) { envelope['presetsPorDispositivo'] = (presetsPorDispositivo ?? {}).map( (deviceId, preset) => MapEntry(deviceId, preset.toJson()), ); envelope['presetsMatriz'] = (presetsMatriz ?? {}).map( (clave, preset) => MapEntry(clave, preset.toJson()), ); envelope['eqMultiDeviceEnabled'] = eqMultiDeviceEnabled ?? false; } // v4 extension: only written when explicitly provided. if (tieneExtensionV4) { envelope['ecualizadorActivo'] = ecualizadorActivo; } return envelope; } /// Serializes an export envelope to pretty-printed JSON (same formatting /// the legacy export shared as a file). String exportar(Map config) => const JsonEncoder.withIndent(' ').convert(config); /// Parses a backup JSON string. Returns `null` on malformed input or when /// the document is not a JSON object — graceful, never throws (S4-R4). Map? importar(String raw) { try { final decoded = jsonDecode(raw); if (decoded is! Map) return null; return Map.from(decoded); } on FormatException { return null; } } }