import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';

import 'package:core/core.dart';
import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
import 'package:email_recovery/email_recovery/email_recovery_action.dart';
import 'package:email_recovery/email_recovery/email_recovery_action_id.dart';
import 'package:email_recovery/email_recovery/get/get_email_recovery_action_method.dart';
import 'package:email_recovery/email_recovery/get/get_email_recovery_action_response.dart';
import 'package:email_recovery/email_recovery/set/set_email_recovery_action_method.dart';
import 'package:email_recovery/email_recovery/set/set_email_recovery_action_response.dart';
import 'package:external_path/external_path.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:jmap_dart_client/http/http_client.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart';
import 'package:jmap_dart_client/jmap/core/capability/core_capability.dart';
import 'package:jmap_dart_client/jmap/core/error/set_error.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/core/patch_object.dart';
import 'package:jmap_dart_client/jmap/core/properties/properties.dart';
import 'package:jmap_dart_client/jmap/core/reference_id.dart';
import 'package:jmap_dart_client/jmap/core/reference_prefix.dart';
import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:jmap_dart_client/jmap/jmap_request.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/email/get/get_email_method.dart';
import 'package:jmap_dart_client/jmap/mail/email/get/get_email_response.dart';
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:jmap_dart_client/jmap/mail/email/parse/parse_email_method.dart';
import 'package:jmap_dart_client/jmap/mail/email/parse/parse_email_response.dart';
import 'package:jmap_dart_client/jmap/mail/email/set/set_email_method.dart';
import 'package:jmap_dart_client/jmap/mail/email/set/set_email_response.dart';
import 'package:jmap_dart_client/jmap/mail/email/submission/address.dart';
import 'package:jmap_dart_client/jmap/mail/email/submission/email_submission.dart';
import 'package:jmap_dart_client/jmap/mail/email/submission/email_submission_id.dart';
import 'package:jmap_dart_client/jmap/mail/email/submission/envelope.dart';
import 'package:jmap_dart_client/jmap/mail/email/submission/set/set_email_submission_method.dart';
import 'package:jmap_dart_client/jmap/mail/email/submission/set/set_email_submission_response.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/set/set_mailbox_method.dart';
import 'package:model/account/account_request.dart';
import 'package:model/account/authentication_type.dart';
import 'package:model/download/download_task_id.dart';
import 'package:model/email/attachment.dart';
import 'package:model/email/mark_star_action.dart';
import 'package:model/email/read_actions.dart';
import 'package:model/extensions/account_id_extensions.dart';
import 'package:model/extensions/email_extension.dart';
import 'package:model/extensions/email_id_extensions.dart';
import 'package:model/extensions/keyword_identifier_extension.dart';
import 'package:model/extensions/list_email_id_extension.dart';
import 'package:model/extensions/list_id_extension.dart';
import 'package:model/extensions/mailbox_id_extension.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:model/oidc/token_oidc.dart';
import 'package:path_provider/path_provider.dart';
import 'package:tmail_ui_user/features/base/mixin/handle_error_mixin.dart';
import 'package:tmail_ui_user/features/composer/domain/exceptions/set_method_exception.dart';
import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart';
import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart';
import 'package:tmail_ui_user/features/email/domain/extensions/email_id_extensions.dart';
import 'package:tmail_ui_user/features/email/domain/model/event_action.dart';
import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart';
import 'package:tmail_ui_user/features/email/domain/model/restore_deleted_message_request.dart';
import 'package:tmail_ui_user/features/email/domain/state/download_all_attachments_for_web_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart';
import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart';
import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart';
import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart';
import 'package:tmail_ui_user/main/error/capability_validator.dart';
import 'package:uri/uri.dart';
import 'package:uuid/uuid.dart';

class EmailAPI with HandleSetErrorMixin {

  final HttpClient _httpClient;
  final DownloadManager _downloadManager;
  final DioClient _dioClient;
  final Uuid _uuid;

  EmailAPI(this._httpClient, this._downloadManager, this._dioClient, this._uuid);

  Future<Email> getEmailContent(
    Session session,
    AccountId accountId,
    EmailId emailId,
    {Properties? additionalProperties}
  ) async {
    final processingInvocation = ProcessingInvocation();

    final jmapRequestBuilder = JmapRequestBuilder(_httpClient, processingInvocation);

    final getEmailMethod = GetEmailMethod(accountId)
      ..addIds({emailId.id})
      ..addProperties(ThreadConstants.propertiesGetEmailContent)
      ..addFetchHTMLBodyValues(true);
    if (additionalProperties != null) {
      getEmailMethod.addProperties(additionalProperties);
    }

    final getEmailInvocation = jmapRequestBuilder.invocation(getEmailMethod);

    final capabilities = getEmailMethod.requiredCapabilities
      .toCapabilitiesSupportTeamMailboxes(session, accountId);

    final result = await (jmapRequestBuilder
        ..usings(capabilities))
      .build()
      .execute();

    final resultList = result.parse<GetEmailResponse>(
      getEmailInvocation.methodCallId,
      GetEmailResponse.deserialize
    );

    if (resultList?.list.isNotEmpty == true) {
      return resultList!.list.first;
    } else {
      throw NotFoundEmailException();
    }
  }

  Future<void> sendEmail(
    Session session,
    AccountId accountId,
    EmailRequest emailRequest,
    {
      CreateNewMailboxRequest? mailboxRequest,
      CancelToken? cancelToken,
    }
  ) async {
    final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());

    Email? emailNeedsToBeCreated;
    MailboxId? outboxMailboxId;

    if (mailboxRequest != null) {
      final generateCreateId = Id(_uuid.v1());
      final setMailboxMethod = SetMailboxMethod(accountId)
        ..addCreate(
            generateCreateId,
            Mailbox(
              name: mailboxRequest.newName,
              parentId: mailboxRequest.parentId,
              isSubscribed: IsSubscribed(mailboxRequest.isSubscribed)
            )
        );

      requestBuilder.invocation(setMailboxMethod);

      outboxMailboxId = MailboxId(ReferenceId(
          ReferencePrefix.defaultPrefix,
          generateCreateId));
      emailNeedsToBeCreated = emailRequest.email.updatedEmail(newMailboxIds: {outboxMailboxId: true});
    } else {
      outboxMailboxId = emailRequest.email.mailboxIds?.keys.first;
      emailNeedsToBeCreated = emailRequest.email;
    }

    final idCreateMethod = Id(_uuid.v1());
    final setEmailMethod = SetEmailMethod(accountId)
      ..addCreate(idCreateMethod, emailNeedsToBeCreated);

    final submissionCreateId = Id(_uuid.v1());
    final mailFrom = Address(emailNeedsToBeCreated.from?.first.email ?? '');
    final recipientsList = emailNeedsToBeCreated.getRecipientEmailAddressList()
      .map((emailAddress) => Address(emailAddress))
      .toSet();
    final emailSubmissionId = EmailSubmissionId(ReferenceId(ReferencePrefix.defaultPrefix, submissionCreateId));
    Map<EmailSubmissionId, PatchObject> mapEmailSubmissionUpdated = {
      emailSubmissionId: PatchObject({
        if (emailRequest.sentMailboxId != null)
          emailRequest.sentMailboxId!.generatePath() : true,
        outboxMailboxId!.generatePath() : null,
        KeyWordIdentifier.emailSeen.generatePath(): true,
        KeyWordIdentifier.emailDraft.generatePath(): null
      })
    };
    final emailSubmission = EmailSubmission(
      identityId: emailRequest.identityId?.id,
      emailId: EmailId(ReferenceId(ReferencePrefix.defaultPrefix, idCreateMethod)),
      envelope: Envelope(mailFrom, recipientsList));

    final setEmailSubmissionMethod = SetEmailSubmissionMethod(accountId)
      ..addCreate(submissionCreateId, emailSubmission)
      ..addOnSuccessUpdateEmail(mapEmailSubmissionUpdated);

    final setEmailInvocation = requestBuilder.invocation(setEmailMethod);
    final setEmailSubmissionInvocation = requestBuilder.invocation(setEmailSubmissionMethod);

    SetEmailMethod? markAsAnsweredOrForwardedSetMethod;
    RequestInvocation? markAsAnsweredOrForwardedInvocation;
    SetEmailResponse? markAsAnsweredOrForwardedSetResponse;

    if (emailRequest.isEmailAnswered) {
      markAsAnsweredOrForwardedSetMethod = SetEmailMethod(accountId)
        ..addUpdates([emailRequest.emailIdAnsweredOrForwarded!].generateMapUpdateObjectMarkAsAnswered());

      markAsAnsweredOrForwardedInvocation = requestBuilder.invocation(markAsAnsweredOrForwardedSetMethod);
    } else if (emailRequest.isEmailForwarded) {
      markAsAnsweredOrForwardedSetMethod = SetEmailMethod(accountId)
        ..addUpdates([emailRequest.emailIdAnsweredOrForwarded!].generateMapUpdateObjectMarkAsForwarded());

      markAsAnsweredOrForwardedInvocation = requestBuilder.invocation(markAsAnsweredOrForwardedSetMethod);
    }

    final capabilities = setEmailSubmissionMethod.requiredCapabilities
      .toCapabilitiesSupportTeamMailboxes(session, accountId);

    final response = await (requestBuilder
        ..usings(capabilities))
      .build()
      .execute(cancelToken: cancelToken);

    final setEmailResponse = response.parse<SetEmailResponse>(
      setEmailInvocation.methodCallId,
      SetEmailResponse.deserialize);

    final setEmailSubmissionResponse = response.parse<SetEmailSubmissionResponse>(
      setEmailSubmissionInvocation.methodCallId,
      SetEmailSubmissionResponse.deserialize,
      methodName: setEmailInvocation.methodName);

    if (markAsAnsweredOrForwardedInvocation != null) {
      markAsAnsweredOrForwardedSetResponse = response.parse<SetEmailResponse>(
        markAsAnsweredOrForwardedInvocation.methodCallId,
        SetEmailResponse.deserialize);
    }

    final emailCreated = setEmailResponse?.created?[idCreateMethod];
    final mapErrors = handleSetResponse([
      setEmailResponse,
      setEmailSubmissionResponse,
      markAsAnsweredOrForwardedSetResponse
    ]);

    if (emailCreated == null || mapErrors.isNotEmpty) {
      throw SetMethodException(mapErrors);
    }
  }

  Future<({
    List<EmailId> emailIdsSuccess,
    Map<Id, SetError> mapErrors,
  })> markAsRead(
    Session session,
    AccountId accountId,
    List<EmailId> emailIds,
    ReadActions readActions,
  ) async {
    final maxObjects = _getMaxObjectsInSetMethod(session, accountId);
    final totalEmails = emailIds.length;
    final maxBatches = min(totalEmails, maxObjects);

    final List<EmailId> updatedEmailIds = List.empty(growable: true);
    final Map<Id, SetError> mapErrors = <Id, SetError>{};

    for (int start = 0; start < totalEmails; start += maxBatches) {
      int end = (start + maxBatches < totalEmails)
        ? start + maxBatches
        : totalEmails;
      log('EmailAPI::markAsRead:emails from ${start + 1} to $end');

      final currentListEmailIds = emailIds.sublist(start, end);

      final setEmailMethod = SetEmailMethod(accountId)
        ..addUpdates(
            currentListEmailIds.generateMapUpdateObjectMarkAsRead(readActions)
          );

      final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());

      final setEmailInvocation = requestBuilder.invocation(setEmailMethod);

      final capabilities = setEmailMethod.requiredCapabilities
          .toCapabilitiesSupportTeamMailboxes(session, accountId);

      final response = await (requestBuilder
          ..usings(capabilities))
        .build()
        .execute();

      final setEmailResponse = response.parse<SetEmailResponse>(
        setEmailInvocation.methodCallId,
        SetEmailResponse.deserialize,
      );

      final listEmailIds = setEmailResponse?.updated?.keys.toEmailIds() ?? [];
      final mapErrors = handleSetResponse([setEmailResponse]);

      updatedEmailIds.addAll(listEmailIds);
      mapErrors.addAll(mapErrors);
    }

    return (emailIdsSuccess: updatedEmailIds, mapErrors: mapErrors);
  }

  Future<List<DownloadTaskId>> downloadAttachments(
      List<Attachment> attachments,
      AccountId accountId,
      String baseDownloadUrl,
      AccountRequest accountRequest
  ) async {
    if (accountRequest.authenticationType == AuthenticationType.oidc &&
        accountRequest.token?.isExpired == true &&
        accountRequest.token?.refreshToken.isNotEmpty == true) {
      throw DownloadAttachmentHasTokenExpiredException(accountRequest.token!.refreshToken);
    }

    String externalStorageDirPath;
    if (Platform.isAndroid) {
      externalStorageDirPath = await ExternalPath.getExternalStoragePublicDirectory(ExternalPath.DIRECTORY_DOWNLOADS);
    } else if (Platform.isIOS) {
      externalStorageDirPath = (await getApplicationDocumentsDirectory()).absolute.path;
    } else {
      throw DeviceNotSupportedException();
    }

    final authentication = accountRequest.authenticationType == AuthenticationType.oidc
        ? accountRequest.bearerToken
        : accountRequest.basicAuth;

    final taskIds = await Future.wait(
      attachments.map((attachment) async => await FlutterDownloader.enqueue(
        url: attachment.getDownloadUrl(baseDownloadUrl, accountId),
        savedDir: externalStorageDirPath,
        headers: {
          HttpHeaders.authorizationHeader: authentication,
          HttpHeaders.acceptHeader: DioClient.jmapHeader
        },
        fileName: attachment.name,
        showNotification: true,
        openFileFromNotification: true)));

    return taskIds
      .where((taskId) => taskId != null)
      .map((taskId) => DownloadTaskId(taskId!))
      .toList();
  }

  Future<DownloadedResponse> exportAttachment(
      Attachment attachment,
      AccountId accountId,
      String baseDownloadUrl,
      AccountRequest accountRequest,
      CancelToken cancelToken
  ) async {
    final authentication = accountRequest.authenticationType == AuthenticationType.oidc
      ? accountRequest.bearerToken
      : accountRequest.basicAuth;

    return _downloadManager.downloadFile(
      attachment.getDownloadUrl(baseDownloadUrl, accountId),
      getTemporaryDirectory(),
      attachment.name ?? '',
      authentication,
      cancelToken: cancelToken);
  }

  Future<Uint8List> downloadAttachmentForWeb(
      DownloadTaskId taskId,
      Attachment attachment,
      AccountId accountId,
      String baseDownloadUrl,
      AccountRequest accountRequest,
      StreamController<Either<Failure, Success>> onReceiveController,
      {CancelToken? cancelToken}
  ) async {
    final authentication = accountRequest.authenticationType == AuthenticationType.oidc
        ? accountRequest.bearerToken
        : accountRequest.basicAuth;
    final downloadUrl = attachment.getDownloadUrl(baseDownloadUrl, accountId);
    log('EmailAPI::downloadAttachmentForWeb(): downloadUrl: $downloadUrl');

    final headerParam = _dioClient.getHeaders();
    headerParam[HttpHeaders.authorizationHeader] = authentication;
    headerParam[HttpHeaders.acceptHeader] = DioClient.jmapHeader;

    final bytesDownloaded = await _dioClient.get(
        downloadUrl,
        options: Options(
            headers: headerParam,
            responseType: ResponseType.bytes),
        cancelToken: cancelToken,
        onReceiveProgress: (downloaded, total) {
          log('EmailAPI::downloadFileForWeb(): downloaded = $downloaded | total: $total');
          double progress = 0;
          if (downloaded > 0 && total >= downloaded) {
            progress = (downloaded / total) * 100;
          }
          log('EmailAPI::downloadFileForWeb(): progress = ${progress.round()}%');
          onReceiveController.add(Right(DownloadingAttachmentForWeb(
              taskId,
              attachment,
              progress,
              downloaded,
              total)));
        }
    );

    return bytesDownloaded;
  }

  Future<void> downloadAllAttachmentsForWeb(
    AccountId accountId,
    EmailId emailId,
    String baseDownloadAllUrl,
    String outputFileName,
    AccountRequest accountRequest,
    DownloadTaskId taskId,
    StreamController<Either<Failure, Success>> onReceiveController,
    {CancelToken? cancelToken}
  ) async {
    final authentication = accountRequest.authenticationType == AuthenticationType.oidc
        ? accountRequest.bearerToken
        : accountRequest.basicAuth;
    final headerParam = _dioClient.getHeaders();
    headerParam[HttpHeaders.authorizationHeader] = authentication;
    headerParam[HttpHeaders.acceptHeader] = DioClient.jmapHeader;
    
    final downloadAllUriTemplate = UriTemplate(Uri.decodeFull(baseDownloadAllUrl));
    final downloadAllUrl = downloadAllUriTemplate.expand({
      'accountId': accountId.asString,
      'emailId': emailId.asString,
      'name': outputFileName,
    });
    final downloadFileName = '$outputFileName.zip';

    final bytesDownloaded = await _dioClient.get(
      downloadAllUrl,
      options: Options(
        headers: headerParam,
        responseType: ResponseType.bytes),
      onReceiveProgress: (downloaded, total) {
        log('EmailAPI::downloadFileForWeb(): downloaded = $downloaded | total: $total');
        double progress = 0;
        if (downloaded > 0 && total >= downloaded) {
          progress = (downloaded / total) * 100;
        }
        log('EmailAPI::downloadFileForWeb(): progress = ${progress.round()}%');
        onReceiveController.add(Right(DownloadingAllAttachmentsForWeb(
          taskId,
          downloadFileName,
          progress,
          downloaded,
          total > 0 ? total : 0,
        )));
      },
      cancelToken: cancelToken,
    );

    _downloadManager.createAnchorElementDownloadFileWeb(
      bytesDownloaded,
      downloadFileName,
    );
  }

  Future<DownloadedResponse> exportAllAttachments(
    AccountId accountId,
    EmailId emailId,
    String baseDownloadAllUrl,
    String outputFileName,
    AccountRequest accountRequest,
    {CancelToken? cancelToken}
  ) async {
    final authentication = accountRequest.authenticationType == AuthenticationType.oidc
      ? accountRequest.bearerToken
      : accountRequest.basicAuth;

    final downloadAllUriTemplate = UriTemplate(Uri.decodeFull(baseDownloadAllUrl));
    final downloadAllUrl = downloadAllUriTemplate.expand({
      'accountId': accountId.asString,
      'emailId': emailId.asString,
      'name': outputFileName,
    });
    final downloadFileName = '$outputFileName.zip';

    return _downloadManager.downloadFile(
      downloadAllUrl,
      getTemporaryDirectory(),
      downloadFileName,
      authentication,
      cancelToken: cancelToken);
  }

  Future<({
    List<EmailId> emailIdsSuccess,
    Map<Id, SetError> mapErrors,
  })> moveToMailbox(
    Session session,
    AccountId accountId,
    MoveToMailboxRequest moveRequest
  ) async {
    final List<EmailId> listEmailIdResult = List.empty(growable: true);
    final Map<Id, SetError> mapErrors = <Id, SetError>{};

    final listMailboxIds = moveRequest.currentMailboxes.keys.toList();
    for (int i = 0; i < listMailboxIds.length; i++) {
      final currentMailboxId = listMailboxIds[i];
      final listEmailIds = moveRequest.currentMailboxes[currentMailboxId]!;
      log('EmailAPI::moveToMailbox:from mailbox ${currentMailboxId.asString} with ${listEmailIds.length} emails to mailbox ${moveRequest.destinationMailboxId.asString}');
      final resultRecords = await _moveEmailsBetweenMailboxes(
        session: session,
        accountId: accountId,
        emailIds: listEmailIds,
        currentMailboxId: currentMailboxId,
        destinationMailboxId: moveRequest.destinationMailboxId,
        isMovingToSpam: moveRequest.isMovingToSpam,
      );

      listEmailIdResult.addAll(resultRecords.emailIdsSuccess);
      mapErrors.addAll(resultRecords.mapErrors);
    }

    return (emailIdsSuccess: listEmailIdResult, mapErrors: mapErrors);
  }

  Future<({
    List<EmailId> emailIdsSuccess,
    Map<Id, SetError> mapErrors,
  })> _moveEmailsBetweenMailboxes({
    required Session session,
    required AccountId accountId,
    required List<EmailId> emailIds,
    required MailboxId currentMailboxId,
    required MailboxId destinationMailboxId,
    bool isMovingToSpam = false,
  }) async {
    final maxObjects = _getMaxObjectsInSetMethod(session, accountId);
    final totalEmails = emailIds.length;
    final maxBatches = min(totalEmails, maxObjects);

    final List<EmailId> updatedEmailIds = List.empty(growable: true);
    final Map<Id, SetError> mapErrors = <Id, SetError>{};

    for (int start = 0; start < totalEmails; start += maxBatches) {
      int end = (start + maxBatches < totalEmails)
          ? start + maxBatches
          : totalEmails;
      log('EmailAPI::_moveEmailsBetweenMailboxes:emails from ${start + 1} to $end');

      final currentEmailIds = emailIds.sublist(start, end);

      final moveProperties = isMovingToSpam
          ? currentEmailIds.generateMapUpdateObjectMoveToSpam(
              currentMailboxId,
              destinationMailboxId,
            )
          : currentEmailIds.generateMapUpdateObjectMoveToMailbox(
              currentMailboxId,
              destinationMailboxId,
            );

      final setEmailMethod = SetEmailMethod(accountId)
        ..addUpdates(moveProperties);

      final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());

      final setEmailInvocation = requestBuilder.invocation(setEmailMethod);

      final capabilities = setEmailMethod.requiredCapabilities
          .toCapabilitiesSupportTeamMailboxes(session, accountId);

      final response = await (requestBuilder
          ..usings(capabilities))
        .build()
        .execute();

      final setEmailResponse = response.parse(
        setEmailInvocation.methodCallId,
        SetEmailResponse.deserialize,
      );

      final listEmailIds = setEmailResponse?.updated?.keys.toEmailIds() ?? [];
      final mapErrors = handleSetResponse([setEmailResponse]);

      updatedEmailIds.addAll(listEmailIds);
      mapErrors.addAll(mapErrors);
    }

    return (emailIdsSuccess: updatedEmailIds, mapErrors: mapErrors);
  }

  int _getMaxObjectsInSetMethod(Session session, AccountId accountId) {
    final coreCapability = session.getCapabilityProperties<CoreCapability>(
      accountId,
      CapabilityIdentifier.jmapCore,
    );
    final maxObjectsInSetMethod = coreCapability?.maxObjectsInSet?.value.toInt()
        ?? CapabilityIdentifierExtension.defaultMaxObjectsInSet;

    final minOfMaxObjectsInSetMethod = min(
      maxObjectsInSetMethod,
      CapabilityIdentifierExtension.defaultMaxObjectsInSet,
    );
    log('EmailAPI::_getMaxObjectsInSetMethod:minOfMaxObjectsInSetMethod = $minOfMaxObjectsInSetMethod');
    return minOfMaxObjectsInSetMethod;
  }

  Future<({
    List<EmailId> emailIdsSuccess,
    Map<Id, SetError> mapErrors,
  })> markAsStar(
    Session session,
    AccountId accountId,
    List<EmailId> emailIds,
    MarkStarAction markStarAction
  ) async {
    final maxObjects = _getMaxObjectsInSetMethod(session, accountId);
    final totalEmails = emailIds.length;
    final maxBatches = min(totalEmails, maxObjects);

    final List<EmailId> updatedEmailIds = List.empty(growable: true);
    final Map<Id, SetError> mapErrors = <Id, SetError>{};

    for (int start = 0; start < totalEmails; start += maxBatches) {
      int end = (start + maxBatches < totalEmails)
        ? start + maxBatches
        : totalEmails;
      log('EmailAPI::markAsStar:emails from ${start + 1} to $end');

      final currentListEmailIds = emailIds.sublist(start, end);

      final setEmailMethod = SetEmailMethod(accountId)
        ..addUpdates(
            currentListEmailIds.generateMapUpdateObjectMarkAsStar(markStarAction),
          );

      final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());

      final setEmailInvocation = requestBuilder.invocation(setEmailMethod);

      final capabilities = setEmailMethod.requiredCapabilities
          .toCapabilitiesSupportTeamMailboxes(session, accountId);

      final response = await (requestBuilder
          ..usings(capabilities))
        .build()
        .execute();

      final setEmailResponse = response.parse<SetEmailResponse>(
        setEmailInvocation.methodCallId,
        SetEmailResponse.deserialize,
      );

      final listEmailIds = setEmailResponse?.updated?.keys.toEmailIds() ?? [];
      final mapErrors = handleSetResponse([setEmailResponse]);

      updatedEmailIds.addAll(listEmailIds);
      mapErrors.addAll(mapErrors);
    }

    return (emailIdsSuccess: updatedEmailIds, mapErrors: mapErrors);
  }

  Future<Email> saveEmailAsDrafts(
    Session session,
    AccountId accountId,
    Email email,
    {CancelToken? cancelToken}
  ) async {
    final idCreateMethod = Id(_uuid.v1());
    final setEmailMethod = SetEmailMethod(accountId)
      ..addCreate(idCreateMethod, email);

    final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());

    final setEmailInvocation = requestBuilder.invocation(setEmailMethod);

    final capabilities = setEmailMethod.requiredCapabilities
      .toCapabilitiesSupportTeamMailboxes(session, accountId);

    final response = await (requestBuilder
        ..usings(capabilities))
      .build()
      .execute(cancelToken: cancelToken);

    final setEmailResponse = response.parse<SetEmailResponse>(
      setEmailInvocation.methodCallId,
      SetEmailResponse.deserialize
    );

    final emailCreated = setEmailResponse?.created?[idCreateMethod];
    final mapErrors = handleSetResponse([setEmailResponse]);

    if (emailCreated != null && mapErrors.isEmpty) {
      return emailCreated;
    } else {
      throw SetMethodException(mapErrors);
    }
  }

  Future<bool> removeEmailDrafts(
    Session session,
    AccountId accountId,
    EmailId emailId,
    {CancelToken? cancelToken}
  ) async {
    final setEmailMethod = SetEmailMethod(accountId)
      ..addDestroy({emailId.id});

    final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());

    final setEmailInvocation = requestBuilder.invocation(setEmailMethod);

    final capabilities = setEmailMethod.requiredCapabilities
      .toCapabilitiesSupportTeamMailboxes(session, accountId);

    final response = await (requestBuilder
        ..usings(capabilities))
      .build()
      .execute(cancelToken: cancelToken);

    final setEmailResponse = response.parse<SetEmailResponse>(
        setEmailInvocation.methodCallId,
        SetEmailResponse.deserialize);

    final isEmailDestroyed = setEmailResponse?.destroyed?.contains(emailId.id) ?? false;
    final mapErrors = handleSetResponse([setEmailResponse]);

    if (isEmailDestroyed && mapErrors.isEmpty) {
      return isEmailDestroyed;
    } else {
      throw SetMethodException(mapErrors);
    }
  }

  Future<Email> updateEmailDrafts(
    Session session,
    AccountId accountId,
    Email newEmail,
    EmailId oldEmailId,
    {CancelToken? cancelToken}
  ) async {
    final emailCreated = await saveEmailAsDrafts(
      session,
      accountId,
      newEmail,
      cancelToken: cancelToken
    );

    try {
      await removeEmailDrafts(
        session,
        accountId,
        oldEmailId,
        cancelToken: cancelToken
      );
    } catch (e) {
      logError('EmailAPI::updateEmailDrafts: Exception = $e');
    }

    return emailCreated;
  }

  Future<({
    List<EmailId> emailIdsSuccess,
    Map<Id, SetError> mapErrors,
  })> deleteMultipleEmailsPermanently(
    Session session,
    AccountId accountId,
    List<EmailId> emailIds
  ) async {
    final maxObjects = _getMaxObjectsInSetMethod(session, accountId);
    final totalEmails = emailIds.length;
    final maxBatches = min(totalEmails, maxObjects);

    final List<EmailId> destroyedEmailIds = List.empty(growable: true);
    final Map<Id, SetError> mapErrors = <Id, SetError>{};

    for (int start = 0; start < totalEmails; start += maxBatches) {
      int end = (start + maxBatches < totalEmails)
          ? start + maxBatches
          : totalEmails;
      log('EmailAPI::deleteMultipleEmailsPermanently:emails from ${start + 1} to $end');

      final currentListEmailIds = emailIds.sublist(start, end);

      final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());
      final setEmailMethod = SetEmailMethod(accountId)
        ..addDestroy(currentListEmailIds.toIds().toSet());

      final setEmailInvocation = requestBuilder.invocation(setEmailMethod);

      final capabilities = setEmailMethod.requiredCapabilities
        .toCapabilitiesSupportTeamMailboxes(session, accountId);

      final response = await (requestBuilder
          ..usings(capabilities))
        .build()
        .execute();

      final setEmailResponse = response.parse<SetEmailResponse>(
        setEmailInvocation.methodCallId,
        SetEmailResponse.deserialize,
      );

      final listEmailIds = setEmailResponse?.destroyed?.toEmailIds() ?? [];
      final mapErrors = handleSetResponse([setEmailResponse]);

      destroyedEmailIds.addAll(listEmailIds);
      mapErrors.addAll(mapErrors);
    }

    return (emailIdsSuccess: destroyedEmailIds, mapErrors: mapErrors);
  }

  Future<bool> deleteEmailPermanently(
    Session session,
    AccountId accountId,
    EmailId emailId,
    {CancelToken? cancelToken}
  ) async {
    final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());
    final setEmailMethod = SetEmailMethod(accountId)
      ..addDestroy({emailId.id});

      final setEmailInvocation = requestBuilder.invocation(setEmailMethod);

      final capabilities = setEmailMethod.requiredCapabilities
          .toCapabilitiesSupportTeamMailboxes(session, accountId);

    final response = await (requestBuilder
        ..usings(capabilities))
      .build()
      .execute(cancelToken: cancelToken);

      final setEmailResponse = response.parse<SetEmailResponse>(
        setEmailInvocation.methodCallId,
        SetEmailResponse.deserialize,
      );

    return setEmailResponse?.destroyed?.contains(emailId.id) == true;
  }

  Future<Email> getDetailedEmailById(
    Session session,
    AccountId accountId,
    EmailId emailId
  ) async {
    final jmapRequestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());

    final getEmailMethod = GetEmailMethod(accountId)
      ..addIds({emailId.id})
      ..addProperties(ThreadConstants.propertiesGetDetailedEmail)
      ..addFetchHTMLBodyValues(true);

    final getEmailInvocation = jmapRequestBuilder.invocation(getEmailMethod);

    final capabilities = getEmailMethod.requiredCapabilities.toCapabilitiesSupportTeamMailboxes(session, accountId);

    final result = await (jmapRequestBuilder
        ..usings(capabilities))
      .build()
      .execute();

    final resultList = result.parse<GetEmailResponse>(
      getEmailInvocation.methodCallId,
      GetEmailResponse.deserialize);

    if (resultList?.list.isNotEmpty == true) {
      return resultList!.list.first;
    } else {
      throw NotFoundEmailException();
    }
  }

  Future<void> unsubscribeMail(Session session, AccountId accountId, EmailId emailId) async {
    final setEmailMethod = SetEmailMethod(accountId)
      ..addUpdates(emailId.generateMapUpdateObjectUnsubscribeMail());

    final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());
    requestBuilder.invocation(setEmailMethod);
    final setEmailInvocation = requestBuilder.invocation(setEmailMethod);

    final capabilities = setEmailMethod.requiredCapabilities.toCapabilitiesSupportTeamMailboxes(session, accountId);

      final response = await (requestBuilder
          ..usings(capabilities))
        .build()
        .execute();

    final setEmailResponse = response.parse<SetEmailResponse>(
      setEmailInvocation.methodCallId,
      SetEmailResponse.deserialize,
    );

    final emailIdUpdated = setEmailResponse?.updated
        ?.keys
        .map((id) => EmailId(id))
        .toList() ?? [];
    final mapErrors = handleSetResponse([setEmailResponse]);

    if (emailIdUpdated.isEmpty) {
      throw SetMethodException(mapErrors);
    }
  }

  Future<EmailRecoveryAction> restoreDeletedMessage(RestoredDeletedMessageRequest restoredDeletedMessageRequest) async {
    final processingInvocation = ProcessingInvocation();
    final requestBuilder = JmapRequestBuilder(_httpClient, processingInvocation);

    final emailRecoveryActionSetMethod = SetEmailRecoveryActionMethod()
      ..addCreate(
        restoredDeletedMessageRequest.createRequestId,
        restoredDeletedMessageRequest.emailRecoveryAction
      );
    final emailRecoveryActionSetInvocation = requestBuilder.invocation(emailRecoveryActionSetMethod);
    final response = await (requestBuilder
        ..usings(emailRecoveryActionSetMethod.requiredCapabilities))
      .build()
      .execute();

    final emailRecoveryActionSetResponse = response.parse<SetEmailRecoveryActionResponse>(
      emailRecoveryActionSetInvocation.methodCallId,
      SetEmailRecoveryActionResponse.deserialize
    );

    return emailRecoveryActionSetResponse!.created![restoredDeletedMessageRequest.createRequestId]!;
  }

  Future<EmailRecoveryAction> getRestoredDeletedMessage(EmailRecoveryActionId emailRecoveryActionId) async {
    final processingInvocation = ProcessingInvocation();
    final requestBuilder = JmapRequestBuilder(_httpClient, processingInvocation);

    final getEmailRecoveryActionMethod = GetEmailRecoveryActionMethod()
      ..addIds({emailRecoveryActionId.id});
    final getEmailRecoveryActionInvocation = requestBuilder.invocation(getEmailRecoveryActionMethod);

    final response = await (requestBuilder
        ..usings(getEmailRecoveryActionMethod.requiredCapabilities))
      .build()
      .execute();

    final getEmailRecoveryActionResponse = response.parse<GetEmailRecoveryActionResponse>(
      getEmailRecoveryActionInvocation.methodCallId,
      GetEmailRecoveryActionResponse.deserialize
    );

    if (getEmailRecoveryActionResponse?.list.isNotEmpty == true) {
      return getEmailRecoveryActionResponse!.list.firstWhere((element) => element.id == emailRecoveryActionId);
    } else {
      throw NotFoundEmailRecoveryActionException();
    }
  }

  Future<void> storeEventAttendanceStatus(
    Session session,
    AccountId accountId,
    EmailId emailId,
    EventActionType eventActionType
  ) async {
    final setEmailMethod = SetEmailMethod(accountId)
      ..addUpdates(emailId.generateMapUpdateObjectEventAttendanceStatus(eventActionType));

    final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());

    final setEmailInvocation = requestBuilder.invocation(setEmailMethod);

    final capabilities = setEmailMethod.requiredCapabilities
      .toCapabilitiesSupportTeamMailboxes(session, accountId);

    final response = await (requestBuilder
        ..usings(capabilities))
      .build()
      .execute();

    final setEmailResponse = response.parse<SetEmailResponse>(
      setEmailInvocation.methodCallId,
      SetEmailResponse.deserialize,
    );

    final emailIdUpdated = setEmailResponse?.updated
        ?.keys
        .map((id) => EmailId(id))
        .toList() ?? [];
    final mapErrors = handleSetResponse([setEmailResponse]);

    if (emailIdUpdated.isEmpty) {
      throw SetMethodException(mapErrors);
    }
  }

  Future<List<Email>> parseEmailByBlobIds(AccountId accountId, Set<Id> blobIds) async {
    final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());
    final parseEmailMethod = ParseEmailMethod(accountId, blobIds)
      ..addProperties(ThreadConstants.propertiesParseEmailByBlobId)
      ..addFetchHTMLBodyValues(true);
    final parseEmailInvocation = requestBuilder.invocation(parseEmailMethod);

    final response = await (requestBuilder
        ..usings(parseEmailMethod.requiredCapabilities))
      .build()
      .execute();

    final parseEmailResponse = response.parse<ParseEmailResponse>(
      parseEmailInvocation.methodCallId,
      ParseEmailResponse.deserialize);

    if (parseEmailResponse?.parsed?.isNotEmpty == true) {
      return parseEmailResponse!.parsed!.values.toList();
    } else if (parseEmailResponse?.notParsable?.isNotEmpty == true) {
      throw NotParsableBlobIdToEmailException(ids: parseEmailResponse!.notParsable!);
    } else if (parseEmailResponse?.notFound?.isNotEmpty == true) {
      throw NotFoundBlobIdException(parseEmailResponse!.notFound!);
    } else {
      throw NotParsableBlobIdToEmailException();
    }
  }
}