import '../domain/domain.dart';
import 'ports.dart';

final class CreateShareResult {
  const CreateShareResult({
    required this.share,
    required this.recipientUserIds,
    required this.unresolvedEmails,
  });

  final Share share;
  final List<String> recipientUserIds;
  final List<String> unresolvedEmails;
}

final class AcceptShareResult {
  const AcceptShareResult({required this.createdResources});

  final List<SyncedResource> createdResources;

  SyncedResource get createdResource => createdResources.single;
}

final class SharePackItemInput {
  const SharePackItemInput({
    required this.resourceType,
    required this.payloadJson,
  });

  final String resourceType;
  final Map<String, Object?> payloadJson;
}

final class CreateShareUseCase {
  const CreateShareUseCase({
    required this.users,
    required this.shares,
    required this.clock,
    required this.ids,
  });

  final UserRepository users;
  final ShareRepository shares;
  final Clock clock;
  final IdGenerator ids;

  Future<CreateShareResult> execute({
    required String senderUserId,
    String shareKind = 'single',
    String? packName,
    required String? resourceType,
    required Map<String, Object?>? payloadJson,
    List<SharePackItemInput> packItems = const [],
    required List<String> recipientEmails,
  }) async {
    final parsedKind = ShareKind.parse(shareKind);
    final shareContent = _shareContent(
      kind: parsedKind,
      packName: packName,
      resourceType: resourceType,
      payloadJson: payloadJson,
      packItems: packItems,
    );
    final emails = _normalizedEmails(recipientEmails);
    if (emails.isEmpty) {
      throw const ValidationException('recipientEmails must not be empty.');
    }

    final recipientUserIds = <String>[];
    final unresolvedEmails = <String>[];
    for (final email in emails) {
      final user = await users.findByEmail(email);
      if (user == null) {
        unresolvedEmails.add(email);
      } else if (user.id != senderUserId &&
          !recipientUserIds.contains(user.id)) {
        recipientUserIds.add(user.id);
      }
    }

    final now = clock.now().toUtc();
    final share = Share(
      id: ids.newId(),
      senderUserId: senderUserId,
      kind: parsedKind,
      resourceType: shareContent.resourceType,
      packName: shareContent.packName,
      payloadJson: shareContent.payloadJson,
      createdAt: now,
    );
    final recipients = [
      for (final recipientUserId in recipientUserIds)
        ShareRecipient(
          id: ids.newId(),
          shareId: share.id,
          recipientUserId: recipientUserId,
        ),
    ];
    await shares.insertShare(share: share, recipients: recipients);
    return CreateShareResult(
      share: share,
      recipientUserIds: recipientUserIds,
      unresolvedEmails: unresolvedEmails,
    );
  }
}

final class ListInboxUseCase {
  const ListInboxUseCase({required this.shares});

  final ShareRepository shares;

  Future<List<ShareInboxItem>> execute({required String recipientUserId}) {
    return shares.listInbox(recipientUserId);
  }
}

final class AcceptShareUseCase {
  const AcceptShareUseCase({
    required this.shares,
    required this.clock,
    required this.ids,
  });

  final ShareRepository shares;
  final Clock clock;
  final IdGenerator ids;

  Future<AcceptShareResult> execute({
    required String shareId,
    required String recipientUserId,
  }) async {
    final share = await shares.findShareById(shareId);
    final recipient = await shares.findRecipient(
      shareId: shareId,
      recipientUserId: recipientUserId,
    );
    if (share == null || recipient == null) {
      throw const ShareNotFoundException();
    }
    if (share.isRevoked) {
      throw const ShareConflictException('Share has been revoked.');
    }
    if (recipient.status != ShareRecipientStatus.pending) {
      throw const ShareConflictException('Share has already been answered.');
    }

    final now = clock.now().toUtc();
    final resourcesToCreate = [
      for (final item in _resourcesFromShare(share))
        SyncedResource(
          serverId: ids.newId(),
          ownerUserId: recipientUserId,
          resourceType: item.resourceType,
          clientId: ids.newId(),
          payloadJson: item.payloadJson,
          schemaVersion: _schemaVersionFromPayload(item.payloadJson),
          clientUpdatedAt: now,
          serverUpdatedAt: now,
        ),
    ];
    final written = await shares.acceptShare(
      recipientId: recipient.id,
      respondedAt: now,
      resources: resourcesToCreate,
    );
    return AcceptShareResult(createdResources: written);
  }
}

final class DeclineShareUseCase {
  const DeclineShareUseCase({required this.shares, required this.clock});

  final ShareRepository shares;
  final Clock clock;

  Future<void> execute({
    required String shareId,
    required String recipientUserId,
  }) async {
    final share = await shares.findShareById(shareId);
    final recipient = await shares.findRecipient(
      shareId: shareId,
      recipientUserId: recipientUserId,
    );
    if (share == null || recipient == null) {
      throw const ShareNotFoundException();
    }
    if (recipient.status != ShareRecipientStatus.pending) {
      throw const ShareConflictException('Share has already been answered.');
    }
    await shares.updateRecipientStatus(
      recipientId: recipient.id,
      status: ShareRecipientStatus.declined,
      respondedAt: clock.now().toUtc(),
    );
  }
}

final class RevokeShareUseCase {
  const RevokeShareUseCase({required this.shares, required this.clock});

  final ShareRepository shares;
  final Clock clock;

  Future<void> execute({
    required String shareId,
    required String senderUserId,
  }) async {
    final share = await shares.findShareById(shareId);
    if (share == null || share.senderUserId != senderUserId) {
      throw const ShareNotFoundException();
    }
    if (share.isRevoked) {
      return;
    }
    await shares.revokeShare(shareId: shareId, revokedAt: clock.now().toUtc());
  }
}

SyncedResourceType _shareResourceType(String value) {
  final type = SyncedResourceType.parse(value);
  if (type != SyncedResourceType.program &&
      type != SyncedResourceType.workoutTemplate) {
    throw const ValidationException(
      'resourceType must be program or workoutTemplate.',
    );
  }
  return type;
}

final class _ShareContent {
  const _ShareContent({
    required this.resourceType,
    required this.packName,
    required this.payloadJson,
  });

  final SyncedResourceType? resourceType;
  final String? packName;
  final Map<String, Object?> payloadJson;
}

_ShareContent _shareContent({
  required ShareKind kind,
  required String? packName,
  required String? resourceType,
  required Map<String, Object?>? payloadJson,
  required List<SharePackItemInput> packItems,
}) {
  return switch (kind) {
    ShareKind.single => _singleShareContent(
      resourceType: resourceType,
      payloadJson: payloadJson,
    ),
    ShareKind.pack => _packShareContent(packName: packName, items: packItems),
  };
}

_ShareContent _singleShareContent({
  required String? resourceType,
  required Map<String, Object?>? payloadJson,
}) {
  final normalizedType = _blankToNull(resourceType);
  if (normalizedType == null) {
    throw const ValidationException('resourceType must not be blank.');
  }
  final payload = payloadJson;
  if (payload == null) {
    throw const ValidationException('payload is required.');
  }
  return _ShareContent(
    resourceType: _shareResourceType(normalizedType),
    packName: null,
    payloadJson: payload,
  );
}

_ShareContent _packShareContent({
  required String? packName,
  required List<SharePackItemInput> items,
}) {
  final normalizedPackName = _blankToNull(packName);
  if (normalizedPackName == null) {
    throw const ValidationException('packName must not be blank.');
  }
  if (items.isEmpty) {
    throw const ValidationException('items must not be empty.');
  }
  return _ShareContent(
    resourceType: null,
    packName: normalizedPackName,
    payloadJson: {
      'items': [
        for (final item in items)
          {
            'resourceType': _shareResourceType(item.resourceType).wireName,
            'payload': item.payloadJson,
          },
      ],
    },
  );
}

final class _ShareResourceData {
  const _ShareResourceData({
    required this.resourceType,
    required this.payloadJson,
  });

  final SyncedResourceType resourceType;
  final Map<String, Object?> payloadJson;
}

List<_ShareResourceData> _resourcesFromShare(Share share) {
  return switch (share.kind) {
    ShareKind.single => [
      _ShareResourceData(
        resourceType: share.resourceType!,
        payloadJson: share.payloadJson,
      ),
    ],
    ShareKind.pack => _packResourcesFromPayload(share.payloadJson),
  };
}

List<_ShareResourceData> _packResourcesFromPayload(
  Map<String, Object?> payloadJson,
) {
  final rawItems = payloadJson['items'];
  if (rawItems is! List || rawItems.isEmpty) {
    throw const ValidationException('Pack share payload must contain items.');
  }
  return [
    for (final rawItem in rawItems)
      if (rawItem is Map)
        _packResourceFromPayload(Map<String, Object?>.from(rawItem))
      else
        throw const ValidationException('Pack item must be a JSON object.'),
  ];
}

_ShareResourceData _packResourceFromPayload(Map<String, Object?> item) {
  final resourceType = item['resourceType'];
  if (resourceType is! String || resourceType.trim().isEmpty) {
    throw const ValidationException(
      'Pack item resourceType must not be blank.',
    );
  }
  final payload = item['payload'];
  if (payload is Map<String, Object?>) {
    return _ShareResourceData(
      resourceType: _shareResourceType(resourceType),
      payloadJson: payload,
    );
  }
  if (payload is Map) {
    return _ShareResourceData(
      resourceType: _shareResourceType(resourceType),
      payloadJson: Map<String, Object?>.from(payload),
    );
  }
  throw const ValidationException('Pack item payload must be a JSON object.');
}

List<String> _normalizedEmails(List<String> emails) {
  return emails
      .map((email) => email.trim().toLowerCase())
      .where((email) => email.isNotEmpty)
      .toSet()
      .toList(growable: false);
}

String? _blankToNull(String? value) {
  final trimmed = value?.trim();
  return trimmed == null || trimmed.isEmpty ? null : trimmed;
}

int _schemaVersionFromPayload(Map<String, Object?> payloadJson) {
  final schemaVersion = payloadJson['schemaVersion'];
  return schemaVersion is int && schemaVersion > 0 ? schemaVersion : 1;
}
