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.createdResource});

  final SyncedResource createdResource;
}

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,
    required String resourceType,
    required Map<String, Object?> payloadJson,
    required List<String> recipientEmails,
  }) async {
    final parsedType = _shareResourceType(resourceType);
    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,
      resourceType: parsedType,
      payloadJson: 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.resources,
    required this.clock,
    required this.ids,
  });

  final ShareRepository shares;
  final SyncedResourceRepository resources;
  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 resource = SyncedResource(
      serverId: ids.newId(),
      ownerUserId: recipientUserId,
      resourceType: share.resourceType,
      clientId: ids.newId(),
      payloadJson: share.payloadJson,
      schemaVersion: _schemaVersionFromPayload(share.payloadJson),
      clientUpdatedAt: now,
      serverUpdatedAt: now,
    );
    final written = await resources.upsertWithLww(resource);
    await shares.updateRecipientStatus(
      recipientId: recipient.id,
      status: ShareRecipientStatus.accepted,
      respondedAt: now,
    );
    return AcceptShareResult(createdResource: written.resource);
  }
}

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;
}

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

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