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; using Wino.Messaging.Server; 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 ISynchronizerFactory _synchronizerFactory; private SynchronizerFactory _concreteSynchronizerFactory; private IImapTestService _imapTestService; private IAccountService _accountService; private IAuthenticationProvider _authenticationProvider; private bool _isInitialized = false; private SynchronizationManager() { } /// /// Initializes the SynchronizationManager with required dependencies. /// This must be called before using any other methods. /// /// 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, IAuthenticationProvider authenticationProvider) { await _initializationSemaphore.WaitAsync(); try { if (_isInitialized) return; _synchronizerFactory = synchronizerFactory ?? throw new ArgumentNullException(nameof(synchronizerFactory)); _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)); // Get all accounts and create synchronizers for them var accounts = await _accountService.GetAccountsAsync(); foreach (var account in accounts) { try { var synchronizer = _concreteSynchronizerFactory.CreateNewSynchronizer(account); _synchronizerCache.TryAdd(account.Id, synchronizer); _logger.Information("Created synchronizer for account {AccountName} ({AccountId})", account.Name, account.Id); } catch (Exception ex) { _logger.Error(ex, "Failed to create synchronizer for account {AccountName} ({AccountId})", account.Name, account.Id); } } _isInitialized = true; _logger.Information("SynchronizationManager initialized with {Count} synchronizers", _synchronizerCache.Count); } 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; } _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); return result; } catch (Exception ex) { _logger.Error(ex, "Mail synchronization failed for account {AccountId}", options.AccountId); return MailSynchronizationResult.Failed; } } /// /// 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 mail action request to the corresponding account's synchronizer. /// /// Request to queue /// Account ID to queue the request for public async Task QueueRequestAsync(IRequestBase request, Guid accountId) { await QueueRequestAsync(request, accountId, triggerSynchronization: true); } /// /// Queues a mail action request to the corresponding account's synchronizer with optional synchronization triggering. /// /// 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) { // Trigger synchronization to execute the queued request _logger.Debug("Triggering synchronization to execute queued request for account {AccountId}", accountId); var synchronizationOptions = 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(synchronizationOptions); } catch (Exception ex) { _logger.Error(ex, "Failed to execute 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); } /// /// 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 (Exception ex) { _logger.Error(ex, "Failed to download MIME message for mail item {MailItemId}", mailItem.Id); return null; } } /// /// Creates a new synchronizer for a newly added account. /// /// Account to create synchronizer for /// Created synchronizer public Task CreateSynchronizerForAccountAsync(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 Task.FromResult(synchronizer); } catch (Exception ex) { _logger.Error(ex, "Failed to create synchronizer for account {AccountName} ({AccountId})", account.Name, account.Id); return Task.FromResult(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 await CreateSynchronizerForAccountAsync(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."); } } }