import 'dart:convert'; import 'package:uto_manager/domain/entities/authorized_rank.dart'; import 'package:uto_manager/domain/entities/doctrinal_branch.dart'; import 'package:uto_manager/domain/entities/echelon.dart'; import 'package:uto_manager/domain/entities/personnel_record.dart'; import 'package:uto_manager/domain/entities/position_slot.dart'; import 'package:uto_manager/domain/entities/unit_node.dart'; import 'package:uto_manager/domain/value_objects/duty_mos.dart'; /// Codec for serializing and deserializing the full unit tree (units + /// position slots + personnel) to/from a structured JSON representation. /// /// The JSON schema is: /// ```json /// { /// "version": 1, /// "exportedAt": "", /// "units": [ { ...UnitNode fields } ], /// "slots": [ { ...PositionSlot fields } ], /// "personnel": [ { ...PersonnelRecord fields } ] /// } /// ``` class UnitTreeCodec { const UnitTreeCodec(); static const int _schemaVersion = 1; // --------------------------------------------------------------------------- // Encode // --------------------------------------------------------------------------- /// Encodes [units], [slots], and [personnel] into a JSON string. String encode({ required List units, required List slots, required List personnel, }) { final map = encodeToMap(units: units, slots: slots, personnel: personnel); return const JsonEncoder.withIndent(' ').convert(map); } /// Encodes [units], [slots], and [personnel] into a [Map]. Map encodeToMap({ required List units, required List slots, required List personnel, }) { return { 'version': _schemaVersion, 'exportedAt': DateTime.now().toUtc().toIso8601String(), 'units': units.map(_encodeUnit).toList(), 'slots': slots.map(_encodeSlot).toList(), 'personnel': personnel.map(_encodePersonnel).toList(), }; } // --------------------------------------------------------------------------- // Decode // --------------------------------------------------------------------------- /// Decodes a JSON string into a [UnitTreeSnapshot]. /// /// Throws [FormatException] if the JSON is malformed or the schema version /// is unsupported. UnitTreeSnapshot decode(String json) { final dynamic raw = jsonDecode(json); if (raw is! Map) { throw const FormatException('Root JSON element must be an object.'); } return decodeFromMap(raw); } /// Decodes a [Map] into a [UnitTreeSnapshot]. UnitTreeSnapshot decodeFromMap(Map map) { final version = map['version'] as int?; if (version == null || version != _schemaVersion) { throw FormatException( 'Unsupported schema version: $version. Expected $_schemaVersion.', ); } final unitsList = (map['units'] as List? ?? []) .cast>() .map(_decodeUnit) .toList(); final slotsList = (map['slots'] as List? ?? []) .cast>() .map(_decodeSlot) .toList(); final personnelList = (map['personnel'] as List? ?? []) .cast>() .map(_decodePersonnel) .toList(); return UnitTreeSnapshot( units: unitsList, slots: slotsList, personnel: personnelList, ); } // --------------------------------------------------------------------------- // Unit encoding/decoding // --------------------------------------------------------------------------- Map _encodeUnit(UnitNode unit) => { 'unitId': unit.unitId, 'echelon': unit.echelon.name, 'branch': unit.branch.name, 'parentUnitId': unit.parentUnitId, 'name': unit.name, 'isKeyLeader': unit.isKeyLeader, }; UnitNode _decodeUnit(Map map) { return UnitNode( unitId: map['unitId'] as String, echelon: Echelon.values.firstWhere((e) => e.name == map['echelon']), branch: DoctrinalBranch.values.firstWhere((b) => b.name == map['branch']), parentUnitId: map['parentUnitId'] as String?, name: map['name'] as String, isKeyLeader: map['isKeyLeader'] as bool? ?? false, ); } // --------------------------------------------------------------------------- // Slot encoding/decoding // --------------------------------------------------------------------------- Map _encodeSlot(PositionSlot slot) => { 'slotId': slot.slotId, 'unitId': slot.unitId, 'positionTitle': slot.positionTitle, 'dutyMos': slot.dutyMos.code, 'authorizedRank': slot.authorizedRank.name, 'isKeyLeader': slot.isKeyLeader, 'assignedPersonnelId': slot.assignedPersonnelId, }; PositionSlot _decodeSlot(Map map) { return PositionSlot( slotId: map['slotId'] as String, unitId: map['unitId'] as String, positionTitle: map['positionTitle'] as String, dutyMos: DutyMos.unvalidated(map['dutyMos'] as String), authorizedRank: AuthorizedRank.values.firstWhere((r) => r.name == map['authorizedRank']), isKeyLeader: map['isKeyLeader'] as bool? ?? false, assignedPersonnelId: map['assignedPersonnelId'] as String?, ); } // --------------------------------------------------------------------------- // Personnel encoding/decoding // --------------------------------------------------------------------------- Map _encodePersonnel(PersonnelRecord record) => { 'soldierId': record.soldierId, 'name': record.name, 'primaryMos': record.primaryMos.code, 'payGrade': record.payGrade.name, 'qualifications': record.qualifications, 'readinessState': record.readinessState.name, 'assignedSlotId': record.assignedSlotId, }; PersonnelRecord _decodePersonnel(Map map) { return PersonnelRecord( soldierId: map['soldierId'] as String, name: map['name'] as String, primaryMos: DutyMos.unvalidated(map['primaryMos'] as String), payGrade: AuthorizedRank.values.firstWhere((r) => r.name == map['payGrade']), qualifications: (map['qualifications'] as List? ?? []).cast(), readinessState: ReadinessState.values.firstWhere( (s) => s.name == map['readinessState'], ), assignedSlotId: map['assignedSlotId'] as String?, ); } } /// A decoded snapshot of the full unit tree. class UnitTreeSnapshot { const UnitTreeSnapshot({ required this.units, required this.slots, required this.personnel, }); final List units; final List slots; final List personnel; }