using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net.Http; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Requests.Bundles; using Wino.Messaging.UI; namespace Wino.Core.Synchronizers; public abstract partial class BaseSynchronizer : ObservableObject, IBaseSynchronizer { protected SemaphoreSlim synchronizationSemaphore = new(1); protected CancellationToken activeSynchronizationCancellationToken; protected List changeRequestQueue = []; private readonly ConcurrentDictionary _pendingMailOperationIds = new(); private readonly ConcurrentDictionary _pendingCalendarOperationIds = new(); private readonly ConcurrentQueue _capturedSynchronizationIssues = new(); protected readonly IMessenger Messenger; public MailAccount Account { get; } private AccountSynchronizerState state; public AccountSynchronizerState State { get { return state; } set { state = value; // Send state changed message with current progress information Messenger.Send(new AccountSynchronizerStateChanged( Account.Id, value, TotalItemsToSync, RemainingItemsToSync, SynchronizationStatus)); } } /// /// Current synchronization status message. /// [ObservableProperty] public partial string SynchronizationStatus { get; set; } = string.Empty; /// /// Total items to download/sync in current operation. /// 0 means no active download or indeterminate progress. /// [ObservableProperty] public partial int TotalItemsToSync { get; set; } /// /// Remaining items to download/sync in current operation. /// [ObservableProperty] public partial int RemainingItemsToSync { get; set; } /// /// Calculated progress percentage (0-100) based on TotalItemsToSync and RemainingItemsToSync. /// Returns -1 for indeterminate progress (when both are 0). /// public double SynchronizationProgress { get { if (TotalItemsToSync == 0 || RemainingItemsToSync == 0) return -1; // Indeterminate return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100; } } protected BaseSynchronizer(MailAccount account, IMessenger messenger) { Account = account; Messenger = messenger ?? WeakReferenceMessenger.Default; } /// /// Resets synchronization progress to default state. /// protected void ResetSyncProgress() { TotalItemsToSync = 0; RemainingItemsToSync = 0; SynchronizationStatus = string.Empty; OnPropertyChanged(nameof(SynchronizationProgress)); } /// /// Updates synchronization progress with current item counts. /// /// Total items to sync /// Remaining items to sync /// Optional status message protected void UpdateSyncProgress(int total, int remaining, string status = "") { TotalItemsToSync = total; RemainingItemsToSync = remaining; SynchronizationStatus = status; OnPropertyChanged(nameof(SynchronizationProgress)); // Send progress update message Messenger.Send(new AccountSynchronizerStateChanged( Account.Id, State, TotalItemsToSync, RemainingItemsToSync, SynchronizationStatus)); } /// /// Queues a single request to be executed in the next synchronization. /// /// Request to execute. public void QueueRequest(IRequestBase request) { changeRequestQueue.Add(request); TrackQueuedRequest(request); } public bool HasPendingOperation(Guid mailUniqueId) => _pendingMailOperationIds.ContainsKey(mailUniqueId); public IReadOnlyCollection GetPendingOperationUniqueIds() => _pendingMailOperationIds.Keys.ToArray(); public bool HasPendingCalendarOperation(Guid calendarItemId) => _pendingCalendarOperationIds.ContainsKey(calendarItemId); public IReadOnlyCollection GetPendingCalendarOperationIds() => _pendingCalendarOperationIds.Keys.ToArray(); protected void TrackQueuedRequest(IRequestBase request) { if (request is IMailActionRequest mailActionRequest) { _pendingMailOperationIds.TryAdd(mailActionRequest.Item.UniqueId, 0); } if (request is ICalendarActionRequest calendarActionRequest) { if (calendarActionRequest.LocalCalendarItemId.HasValue) { _pendingCalendarOperationIds.TryAdd(calendarActionRequest.LocalCalendarItemId.Value, 0); } } } protected void UntrackProcessedRequest(IRequestBase request) { if (request is IMailActionRequest mailActionRequest) { _pendingMailOperationIds.TryRemove(mailActionRequest.Item.UniqueId, out _); } if (request is ICalendarActionRequest calendarActionRequest) { if (calendarActionRequest.LocalCalendarItemId.HasValue) { _pendingCalendarOperationIds.TryRemove(calendarActionRequest.LocalCalendarItemId.Value, out _); } } } protected void UntrackProcessedRequests(IEnumerable requests) { foreach (var request in requests) UntrackProcessedRequest(request); } protected void ResetCapturedSynchronizationIssues() { while (_capturedSynchronizationIssues.TryDequeue(out _)) { } } protected void CaptureSynchronizationIssue(SynchronizationIssue issue) { if (issue == null || string.IsNullOrWhiteSpace(issue.Message)) return; _capturedSynchronizationIssues.Enqueue(issue); } protected void CaptureSynchronizationIssue(SynchronizerErrorContext errorContext) => CaptureSynchronizationIssue(SynchronizationIssue.FromErrorContext(errorContext)); protected IReadOnlyList GetCapturedSynchronizationIssues() => _capturedSynchronizationIssues.ToArray(); /// /// Runs existing queued requests in the queue. /// /// Batched requests to execute. Integrator methods will only receive batched requests. /// Cancellation token public abstract Task ExecuteNativeRequestsAsync(List> batchedRequests, CancellationToken cancellationToken = default); /// /// Refreshes remote mail account profile if possible. /// Profile picture, sender name and mailbox settings (todo) will be handled in this step. /// public virtual Task GetProfileInformationAsync() => default; /// /// Safely updates account's profile information. /// Database changes are reflected after this call. /// protected async Task SynchronizeProfileInformationInternalAsync() { var profileInformation = await GetProfileInformationAsync(); if (profileInformation != null) { Account.SenderName = profileInformation.SenderName; Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData; if (!string.IsNullOrEmpty(profileInformation.AccountAddress)) { Account.Address = profileInformation.AccountAddress; } } return profileInformation; } /// /// Returns the base64 encoded profile picture of the account from the given URL. /// /// URL to retrieve picture from. /// base64 encoded profile picture protected async Task GetProfilePictureBase64EncodedAsync(string url) { using var client = new HttpClient(); var response = await client.GetAsync(url).ConfigureAwait(false); var byteContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); return Convert.ToBase64String(byteContent); } public List> ForEachRequest(IEnumerable requests, Func action) where TWinoRequestType : IRequestBase { List> ret = []; foreach (var request in requests) ret.Add(new HttpRequestBundle(action(request), request, request)); return ret; } }