class StatsSummary { final List groups; const StatsSummary({required this.groups}); static const empty = StatsSummary(groups: []); static StatsSummary fromApi(Object? data) { return StatsSummary(groups: GroupStats.listFromApi(data)); } } class GroupStats { final String targetId; final String name; final int totalCommands; final int togglesOn; final int togglesOff; final double? estimatedHours; const GroupStats({ required this.targetId, required this.name, required this.totalCommands, required this.togglesOn, required this.togglesOff, this.estimatedHours, }); String get formattedEstimatedHours { final value = estimatedHours; if (value == null) return ''; if (value < 1) return '${(value * 60).round()} мин'; return '${value.toStringAsFixed(1)} ч'; } static GroupStats fromApi(Object? data, {String? fallbackId}) { if (data is! Map) { final id = data?.toString() ?? fallbackId; if (id == null || id.isEmpty) { throw const FormatException('group stats должен быть объектом'); } return GroupStats( targetId: id, name: id, totalCommands: 0, togglesOn: 0, togglesOff: 0, ); } final map = Map.from(data); final targetId = _stringValue(map, const ['target_id', 'group_id', 'id', 'target']) ?? fallbackId; if (targetId == null || targetId.isEmpty) { throw const FormatException('group stats не содержит id'); } return GroupStats( targetId: targetId, name: _stringValue(map, const ['name', 'label']) ?? targetId, totalCommands: _intValue(map['total_commands']), togglesOn: _intValue(map['toggles_on']), togglesOff: _intValue(map['toggles_off']), estimatedHours: _doubleValue(map['estimated_hours']), ); } static List listFromApi(Object? data) { final values = _collectionValues(data, const ['groups', 'data', 'items']); return values.map((value) { if (value.entryKey == null) return GroupStats.fromApi(value.value); return GroupStats.fromApi(value.value, fallbackId: value.entryKey); }).toList(); } } String? _stringValue(Map map, List keys) { for (final key in keys) { final value = map[key]; if (value != null && value.toString().isNotEmpty) { return value.toString(); } } return null; } int _intValue(Object? value) { if (value is int) return value; if (value is num) return value.round(); if (value is String) return int.tryParse(value.trim()) ?? 0; return 0; } double? _doubleValue(Object? value) { if (value is num) return value.toDouble(); if (value is String) return double.tryParse(value.trim()); return null; } List<_CollectionValue> _collectionValues(Object? data, List wrappers) { if (data is List) { return data.map((value) => _CollectionValue(value)).toList(); } if (data is Map) { final map = Map.from(data); for (final wrapper in wrappers) { final value = map[wrapper]; if (value is List) { return value.map((item) => _CollectionValue(item)).toList(); } } if (_looksLikeStatsItem(map)) { return <_CollectionValue>[_CollectionValue(map)]; } return map.entries .where((entry) => entry.value is Map) .map((entry) => _CollectionValue(entry.value, entryKey: entry.key)) .toList(); } throw const FormatException('stats summary должен быть объектом или списком'); } bool _looksLikeStatsItem(Map map) { return map.containsKey('total_commands') || map.containsKey('toggles_on') || map.containsKey('toggles_off') || map.containsKey('estimated_hours'); } class _CollectionValue { final Object? value; final String? entryKey; const _CollectionValue(this.value, {this.entryKey}); }