import 'dart:convert'; import '../../application/application.dart'; import '../../domain/domain.dart'; import 'http_api_client.dart'; final class HttpRemoteShareApi implements RemoteShareApi { const HttpRemoteShareApi(this.client); final HttpApiClient client; @override Future sendShare({ required ShareResourceType resourceType, required Map payload, required List recipientEmails, required String token, }) async { final response = await client.postJson( '/shares', bearerToken: token, body: _shareRequestBody( resourceType: resourceType, payload: payload, recipientEmails: recipientEmails, ), expectedStatuses: const {201}, ); return RemoteShareSendResult( shareId: _requiredString(response, 'shareId'), recipientUserIds: _stringList(response, 'recipientUserIds'), unresolvedEmails: _stringList(response, 'unresolvedEmails'), ); } @override Future> fetchInbox(String token) async { final response = await client.getJson('/shares/inbox', bearerToken: token); return _list( response, 'items', ).map((item) => _inboxItemFromJson(_map(item))).toList(); } @override Future> acceptShare( String shareId, String token, ) async { final response = await client.postJson( '/shares/$shareId/accept', bearerToken: token, ); final resources = _list( response, 'createdResources', ).map((item) => _syncedItemFromJson(_map(item))).toList(growable: false); if (resources.isNotEmpty) { return resources; } return [_syncedItemFromJson(_map(response['createdResource']))]; } @override Future declineShare(String shareId, String token) { return client.postEmpty('/shares/$shareId/decline', bearerToken: token); } @override Future revokeShare(String shareId, String token) { return client.postEmpty('/shares/$shareId/revoke', bearerToken: token); } } Map _shareRequestBody({ required ShareResourceType resourceType, required Map payload, required List recipientEmails, }) { if (resourceType != ShareResourceType.pack) { return { 'shareKind': 'single', 'resourceType': _shareResourceTypeToWire(resourceType), 'payload': payload, 'recipientEmails': recipientEmails, }; } final rawWorkouts = payload['workouts']; if (rawWorkouts is! List) { throw const RemoteAuthException( RemoteAuthFailure.unknown, 'Pack payload must contain workouts.', ); } return { 'shareKind': 'pack', 'packName': _stringFromObject(payload['name'], 'Pack'), 'items': [ for (final rawWorkout in rawWorkouts) {'resourceType': 'workoutTemplate', 'payload': _map(rawWorkout)}, ], 'recipientEmails': recipientEmails, }; } ShareInboxItem _inboxItemFromJson(Map json) { final resourceType = _inboxResourceType(json); return ShareInboxItem( shareId: _requiredString(json, 'shareId'), senderUserId: _requiredString(json, 'senderUserId'), resourceType: resourceType, payloadJson: _payloadJsonString(json, resourceType), status: _shareInboxStatusFromWire(_requiredString(json, 'status')), createdAt: _requiredDateTime(json, 'createdAt'), respondedAt: _optionalDateTime(json, 'respondedAt'), ); } ShareResourceType _inboxResourceType(Map json) { final shareKind = json['shareKind']; if (shareKind == 'pack') { return ShareResourceType.pack; } return _shareResourceTypeFromWire(_requiredString(json, 'resourceType')); } String _payloadJsonString( Map json, ShareResourceType resourceType, ) { final payload = _map(json['payload']); if (resourceType != ShareResourceType.pack) { return jsonEncode(payload); } if (payload['workouts'] is List) { return jsonEncode({ 'name': json['packName'] as String? ?? payload['name'] ?? 'Pack', ...payload, }); } final rawItems = payload['items']; if (rawItems is! List) { return jsonEncode({ 'name': json['packName'] as String? ?? 'Pack', 'workouts': const [], }); } return jsonEncode({ 'name': json['packName'] as String? ?? 'Pack', 'workouts': [ for (final rawItem in rawItems) if (rawItem is Map && Map.from(rawItem)['resourceType'] == 'workoutTemplate') _map(Map.from(rawItem)['payload']), ], }); } RemoteSyncedItem _syncedItemFromJson(Map json) { return RemoteSyncedItem( resourceType: _syncResourceTypeFromWire( _requiredString(json, 'resourceType'), ), clientId: _requiredString(json, 'clientId'), serverId: _requiredString(json, 'serverId'), schemaVersion: _requiredInt(json, 'schemaVersion'), clientUpdatedAt: _requiredDateTime(json, 'clientUpdatedAt'), serverUpdatedAt: _requiredDateTime(json, 'serverUpdatedAt'), deletedAt: _optionalDateTime(json, 'deletedAt'), payload: _map(json['payload']), ); } String _shareResourceTypeToWire(ShareResourceType type) => switch (type) { ShareResourceType.program => 'program', ShareResourceType.workoutTemplate => 'workoutTemplate', ShareResourceType.pack => 'pack', }; ShareResourceType _shareResourceTypeFromWire(String value) => switch (value) { 'program' => ShareResourceType.program, 'workoutTemplate' => ShareResourceType.workoutTemplate, 'pack' => ShareResourceType.pack, _ => throw RemoteAuthException( RemoteAuthFailure.unknown, 'Unknown share resource type: $value', ), }; ShareInboxStatus _shareInboxStatusFromWire(String value) => switch (value) { 'pending' => ShareInboxStatus.pending, 'accepted' => ShareInboxStatus.accepted, 'declined' => ShareInboxStatus.declined, 'revoked' => ShareInboxStatus.revoked, _ => throw RemoteAuthException( RemoteAuthFailure.unknown, 'Unknown share status: $value', ), }; SyncResourceType _syncResourceTypeFromWire(String value) => switch (value) { 'exercise' => SyncResourceType.exercise, 'program' => SyncResourceType.program, 'workoutTemplate' => SyncResourceType.workoutTemplate, 'workoutHistory' => SyncResourceType.workoutHistory, 'mediaAsset' => SyncResourceType.mediaAsset, _ => throw RemoteAuthException( RemoteAuthFailure.unknown, 'Unknown sync resource type: $value', ), }; List _list(Map json, String key) { final value = json[key]; if (value is List) { return value.cast(); } return const []; } List _stringList(Map json, String key) { return _list(json, key).whereType().toList(growable: false); } Map _map(Object? value) { if (value is Map) { return Map.from(value); } throw const RemoteAuthException( RemoteAuthFailure.unknown, 'Expected JSON object.', ); } String _requiredString(Map json, String key) { final value = json[key]; if (value is String && value.trim().isNotEmpty) { return value; } throw RemoteAuthException( RemoteAuthFailure.unknown, '$key is missing from share response.', ); } String _stringFromObject(Object? value, String fallback) { return value is String && value.trim().isNotEmpty ? value : fallback; } int _requiredInt(Map json, String key) { final value = json[key]; if (value is int) { return value; } throw RemoteAuthException( RemoteAuthFailure.unknown, '$key is missing from share response.', ); } DateTime _requiredDateTime(Map json, String key) { return DateTime.parse(_requiredString(json, key)).toUtc(); } DateTime? _optionalDateTime(Map json, String key) { final value = json[key]; return value is String && value.trim().isNotEmpty ? DateTime.parse(value).toUtc() : null; }