using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Serilog; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Authentication; using Wino.Core.Domain.Models.Connectivity; using Wino.Core.Domain.Models.Synchronization; namespace Wino.Core.Services; /// /// Singleton manager that handles synchronizer instances and operations for all accounts. /// Replaces the old WinoServerConnectionManager functionality. /// public class SynchronizationManager : ISynchronizationManager { private static readonly Lazy _instance = new(() => new SynchronizationManager()); public static SynchronizationManager Instance => _instance.Value; private readonly ConcurrentDictionary _synchronizerCache = new(); private readonly SemaphoreSlim _initializationSemaphore = new(1, 1); private readonly ILogger _logger = Log.ForContext(); private SynchronizerFactory _concreteSynchronizerFactory; private IImapTestService _imapTestService; private IAccountService _accountService; private IAuthenticationProvider _authenticationProvider; private INotificationBuilder _notificationBuilder; private bool _isInitialized = false; private SynchronizationManager() { } /// /// Initializes the SynchronizationManager with required dependencies. /// This must be called before using any other methods. /// Note: Synchronizers are created lazily to avoid requiring window handles during app initialization. /// /// Factory for creating synchronizers /// Service for testing IMAP connectivity /// Service for account operations /// Provider for OAuth authentication public async Task InitializeAsync(ISynchronizerFactory synchronizerFactory, IImapTestService imapTestService, IAccountService accountService, INotificationBuilder notificationBuilder, IAuthenticationProvider authenticationProvider) { await _initializationSemaphore.WaitAsync(); try { if (_isInitialized) return; _concreteSynchronizerFactory = synchronizerFactory as SynchronizerFactory ?? throw new ArgumentException("SynchronizerFactory must be the concrete implementation"); _imapTestService = imapTestService ?? throw new ArgumentNullException(nameof(imapTestService)); _accountService = accountService ?? throw new ArgumentNullException(nameof(accountService)); _authenticationProvider = authenticationProvider ?? throw new ArgumentNullException(nameof(authenticationProvider)); _notificationBuilder = notificationBuilder ?? throw new ArgumentNullException(nameof(notificationBuilder)); // DO NOT create synchronizers here to avoid requiring window handles during initialization. // Synchronizers will be created lazily when first accessed via GetOrCreateSynchronizerAsync. _isInitialized = true; _logger.Information("SynchronizationManager dependencies initialized. Synchronizers will be created lazily."); } finally { _initializationSemaphore.Release(); } } /// /// Tests IMAP server connectivity for the given server information. /// /// Server information to test /// Whether to allow SSL handshake /// Test results indicating success or failure with details public async Task TestImapConnectivityAsync(CustomServerInformation serverInformation, bool allowSSLHandshake) { EnsureInitialized(); try { _logger.Information("Testing IMAP connectivity for {Server}:{Port}", serverInformation.IncomingServer, serverInformation.IncomingServerPort); await _imapTestService.TestImapConnectionAsync(serverInformation, allowSSLHandshake); _logger.Information("IMAP connectivity test successful"); return ImapConnectivityTestResults.Success(); } catch (ImapTestSSLCertificateException sslTestException) { _logger.Warning("IMAP connectivity test requires SSL certificate confirmation"); return ImapConnectivityTestResults.CertificateUIRequired( sslTestException.Issuer, sslTestException.ExpirationDateString, sslTestException.ValidFromDateString); } catch (ImapClientPoolException clientPoolException) { _logger.Error(clientPoolException, "IMAP connectivity test failed with protocol log"); return ImapConnectivityTestResults.Failure(clientPoolException, clientPoolException.ProtocolLog); } catch (Exception exception) { _logger.Error(exception, "IMAP connectivity test failed"); return ImapConnectivityTestResults.Failure(exception, string.Empty); } } /// /// Starts a new mail synchronization for the given account. /// /// Mail synchronization options /// Cancellation token /// Synchronization result public async Task SynchronizeMailAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default) { EnsureInitialized(); var synchronizer = await GetOrCreateSynchronizerAsync(options.AccountId); if (synchronizer == null) { _logger.Error("Could not find or create synchronizer for account {AccountId}", options.AccountId); return MailSynchronizationResult.Failed(new Exception("Can't create/get synchronizer.")); } _logger.Information("Starting mail synchronization for account {AccountId} with type {SyncType}", options.AccountId, options.Type); try { var result = await synchronizer.SynchronizeMailsAsync(options, cancellationToken); _logger.Information("Mail synchronization completed for account {AccountId} with state {State}", options.AccountId, result.CompletedState); // Create notifications. if (result.DownloadedMessages?.Any() ?? false) await _notificationBuilder.CreateNotificationsAsync(result.DownloadedMessages); await _notificationBuilder.UpdateTaskbarIconBadgeAsync(); return result; } catch (AuthenticationAttentionException authEx) { _logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId); // Create app notification for authentication attention _notificationBuilder.CreateAttentionRequiredNotification(authEx.Account); return MailSynchronizationResult.Failed(authEx); } catch (Exception ex) { _logger.Error(ex, "Mail synchronization failed for account {AccountId}", options.AccountId); return MailSynchronizationResult.Failed(ex); } } /// /// Checks if there is an ongoing synchronization for the given account. /// /// Account ID to check /// True if synchronization is ongoing, false otherwise public bool IsAccountSynchronizing(Guid accountId) { EnsureInitialized(); if (_synchronizerCache.TryGetValue(accountId, out var synchronizer)) { return synchronizer.State == AccountSynchronizerState.Synchronizing || synchronizer.State == AccountSynchronizerState.ExecutingRequests; } return false; } /// /// Queues a request to the corresponding account's synchronizer with optional synchronization triggering. /// Automatically determines whether to trigger mail or calendar synchronization based on the request type. /// /// Request to queue /// Account ID to queue the request for /// Whether to automatically trigger synchronization after queuing the request public async Task QueueRequestAsync(IRequestBase request, Guid accountId, bool triggerSynchronization) { EnsureInitialized(); var synchronizer = await GetOrCreateSynchronizerAsync(accountId); if (synchronizer == null) { _logger.Error("Could not find or create synchronizer for account {AccountId} to queue request", accountId); return; } _logger.Debug("Queuing request {RequestType} for account {AccountId}", request.GetType().Name, accountId); synchronizer.QueueRequest(request); if (triggerSynchronization) { // Determine if this is a calendar or mail operation bool isCalendarOperation = request is ICalendarActionRequest; if (isCalendarOperation) { // Trigger calendar synchronization _logger.Debug("Triggering calendar synchronization to execute queued request for account {AccountId}", accountId); var calendarSyncOptions = new CalendarSynchronizationOptions() { AccountId = accountId }; // Trigger synchronization asynchronously without waiting for completion _ = Task.Run(async () => { try { await SynchronizeCalendarAsync(calendarSyncOptions); } catch (Exception ex) { _logger.Error(ex, "Failed to execute calendar synchronization after queuing request for account {AccountId}", accountId); } }); } else { // Trigger mail synchronization (includes mail and folder operations) _logger.Debug("Triggering mail synchronization to execute queued request for account {AccountId}", accountId); var mailSyncOptions = new MailSynchronizationOptions() { AccountId = accountId, Type = MailSynchronizationType.ExecuteRequests }; // Trigger synchronization asynchronously without waiting for completion // This matches the pattern used in WinoRequestDelegator _ = Task.Run(async () => { try { await SynchronizeMailAsync(mailSyncOptions); } catch (Exception ex) { _logger.Error(ex, "Failed to execute mail synchronization after queuing request for account {AccountId}", accountId); } }); } } } /// /// Handles folder synchronization for the given account. /// /// Account ID to synchronize folders for /// Cancellation token /// Synchronization result public async Task SynchronizeFoldersAsync(Guid accountId, CancellationToken cancellationToken = default) { EnsureInitialized(); var options = new MailSynchronizationOptions { AccountId = accountId, Type = MailSynchronizationType.FoldersOnly }; return await SynchronizeMailAsync(options, cancellationToken); } /// /// Handles alias synchronization for the given account. /// /// Account ID to synchronize aliases for /// Cancellation token /// Synchronization result public async Task SynchronizeAliasesAsync(Guid accountId, CancellationToken cancellationToken = default) { EnsureInitialized(); var options = new MailSynchronizationOptions { AccountId = accountId, Type = MailSynchronizationType.Alias }; return await SynchronizeMailAsync(options, cancellationToken); } /// /// Handles profile synchronization for the given account. /// /// Account ID to synchronize profile for /// Cancellation token /// Synchronization result public async Task SynchronizeProfileAsync(Guid accountId, CancellationToken cancellationToken = default) { EnsureInitialized(); var options = new MailSynchronizationOptions { AccountId = accountId, Type = MailSynchronizationType.UpdateProfile }; return await SynchronizeMailAsync(options, cancellationToken); } /// /// Handles calendar synchronization for the given account. /// /// Calendar synchronization options /// Cancellation token /// Synchronization result public async Task SynchronizeCalendarAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default) { EnsureInitialized(); var synchronizer = await GetOrCreateSynchronizerAsync(options.AccountId); if (synchronizer == null) { _logger.Error("Could not find or create synchronizer for account {AccountId}", options.AccountId); return CalendarSynchronizationResult.Failed; } _logger.Information("Starting calendar synchronization for account {AccountId} with type {SyncType}", options.AccountId, options.Type); try { var result = await synchronizer.SynchronizeCalendarEventsAsync(options, cancellationToken); _logger.Information("Calendar synchronization completed for account {AccountId} with state {State}", options.AccountId, result.CompletedState); // TODO: Create notifications for new calendar events when INotificationBuilder supports it // if (result.DownloadedEvents?.Any() ?? false) // await _notificationBuilder.CreateCalendarNotificationsAsync(result.DownloadedEvents); return result; } catch (AuthenticationAttentionException authEx) { _logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId); // Create app notification for authentication attention _notificationBuilder.CreateAttentionRequiredNotification(authEx.Account); return CalendarSynchronizationResult.Failed; } catch (Exception ex) { _logger.Error(ex, "Calendar synchronization failed for account {AccountId}", options.AccountId); return CalendarSynchronizationResult.Failed; } } /// /// Downloads a MIME message for the given mail item. /// /// Mail item to download /// Account ID that owns the mail item /// Cancellation token /// Downloaded MIME content path public async Task DownloadMimeMessageAsync(MailCopy mailItem, Guid accountId, CancellationToken cancellationToken = default) { EnsureInitialized(); var synchronizer = await GetOrCreateSynchronizerAsync(accountId); if (synchronizer == null) { _logger.Error("Could not find or create synchronizer for account {AccountId} to download MIME", accountId); return null; } _logger.Debug("Downloading MIME message for mail item {MailItemId}", mailItem.Id); try { await synchronizer.DownloadMissingMimeMessageAsync(mailItem, null, cancellationToken); return mailItem.Id.ToString(); // Return some identifier, actual implementation might be different } catch (SynchronizerEntityNotFoundException) { _logger.Warning("MIME message for mail item {MailItemId} no longer exists on server. Removed locally.", mailItem.Id); return null; } catch (Exception ex) { _logger.Error(ex, "Failed to download MIME message for mail item {MailItemId}", mailItem.Id); return null; } } /// /// Downloads a calendar attachment using the appropriate synchronizer. /// public async Task DownloadCalendarAttachmentAsync( Wino.Core.Domain.Entities.Calendar.CalendarItem calendarItem, Wino.Core.Domain.Entities.Calendar.CalendarAttachment attachment, string localFilePath, CancellationToken cancellationToken = default) { EnsureInitialized(); if (calendarItem == null) throw new ArgumentNullException(nameof(calendarItem)); if (attachment == null) throw new ArgumentNullException(nameof(attachment)); var accountId = calendarItem.AssignedCalendar?.AccountId ?? Guid.Empty; if (accountId == Guid.Empty) throw new InvalidOperationException("Calendar item does not have an assigned account."); var synchronizer = await GetOrCreateSynchronizerAsync(accountId); if (synchronizer == null) { _logger.Error("Could not find or create synchronizer for account {AccountId} to download calendar attachment", accountId); throw new InvalidOperationException("No synchronizer available for downloading calendar attachment."); } _logger.Debug("Downloading calendar attachment {AttachmentId} for calendar item {CalendarItemId}", attachment.Id, calendarItem.Id); try { await synchronizer.DownloadCalendarAttachmentAsync( calendarItem, attachment, localFilePath, cancellationToken); } catch (Exception ex) { _logger.Error(ex, "Failed to download calendar attachment {AttachmentId}", attachment.Id); throw; } } /// /// Creates a new synchronizer for a newly added account. /// /// Account to create synchronizer for /// Created synchronizer public IWinoSynchronizerBase CreateSynchronizerForAccount(MailAccount account) { EnsureInitialized(); try { var synchronizer = _concreteSynchronizerFactory.CreateNewSynchronizer(account); _synchronizerCache.TryAdd(account.Id, synchronizer); _logger.Information("Created new synchronizer for account {AccountName} ({AccountId})", account.Name, account.Id); return synchronizer; } catch (Exception ex) { _logger.Error(ex, "Failed to create synchronizer for account {AccountName} ({AccountId})", account.Name, account.Id); return null; } } /// /// Destroys the synchronizer for the given account. /// /// Account ID to destroy synchronizer for public async Task DestroySynchronizerAsync(Guid accountId) { EnsureInitialized(); if (_synchronizerCache.TryRemove(accountId, out var synchronizer)) { try { await synchronizer.KillSynchronizerAsync(); _logger.Information("Destroyed synchronizer for account {AccountId}", accountId); } catch (Exception ex) { _logger.Error(ex, "Failed to destroy synchronizer for account {AccountId}", accountId); } } } /// /// Gets all cached synchronizers. /// /// Collection of all cached synchronizers public IEnumerable GetAllSynchronizers() { EnsureInitialized(); return _synchronizerCache.Values.ToList(); } /// /// Gets a synchronizer for the given account ID. /// /// Account ID /// Synchronizer if found, null otherwise public async Task GetSynchronizerAsync(Guid accountId) { EnsureInitialized(); return await GetOrCreateSynchronizerAsync(accountId); } private async Task GetOrCreateSynchronizerAsync(Guid accountId) { if (_synchronizerCache.TryGetValue(accountId, out var existingSynchronizer)) { return existingSynchronizer; } // Try to create a new synchronizer if not found var account = await _accountService.GetAccountAsync(accountId); if (account != null) { return CreateSynchronizerForAccount(account); } return null; } /// /// Handles OAuth authentication for the specified provider. /// /// The mail provider type to authenticate /// Optional account to authenticate (null for initial authentication) /// Whether to propose copying auth URL for Gmail /// Token information containing access token and username public async Task HandleAuthorizationAsync(MailProviderType providerType, MailAccount account = null, bool proposeCopyAuthorizationURL = false) { EnsureInitialized(); try { var authenticator = _authenticationProvider.GetAuthenticator(providerType); // Some users are having issues with Gmail authentication. // Their browsers may never launch to complete authentication. // Offer to copy auth url for them to complete it manually. // Redirection will occur to the app and the token will be saved. if (proposeCopyAuthorizationURL && authenticator is IGmailAuthenticator gmailAuthenticator) { gmailAuthenticator.ProposeCopyAuthURL = true; } TokenInformationEx tokenInfo; if (account != null) { // Get token for existing account (may trigger interactive auth if token is expired) tokenInfo = await authenticator.GetTokenInformationAsync(account); _logger.Information("Retrieved token for existing account {AccountAddress}", account.Address); } else { // Initial authentication request - there is no account to get token for // This will always trigger interactive authentication tokenInfo = await authenticator.GenerateTokenInformationAsync(null); _logger.Information("Generated new token for {ProviderType} authentication", providerType); } return tokenInfo; } catch (Exception ex) { _logger.Error(ex, "Failed to handle authorization for {ProviderType}", providerType); throw; } } private void EnsureInitialized() { if (!_isInitialized) { throw new InvalidOperationException("SynchronizationManager must be initialized before use. Call InitializeAsync first."); } } }