using CredentialManager.Models; using DataConnection.CredentialManagement.Interfaces; using DataConnection.REST.Implementations; using DataConnection.REST.Interfaces; using Microsoft.Extensions.Logging; namespace Data_Coupler.Services; /// /// Interfaccia per il servizio di sincronizzazione delle cancellazioni /// public interface IDeletionSyncService { /// /// Sincronizza le cancellazioni dalla sorgente alla destinazione /// Task SyncDeletionsAsync( List currentSourceKeyValues, string destinationEntity, string restCredentialName, IRestServiceClient restClient, DeletionSyncOptions options); } /// /// Servizio per la sincronizzazione delle cancellazioni dalla sorgente alla destinazione /// public class DeletionSyncService : IDeletionSyncService { private readonly IDataConnectionCredentialService _credentialService; private readonly ILogger _logger; public DeletionSyncService( IDataConnectionCredentialService credentialService, ILogger logger) { _credentialService = credentialService ?? throw new ArgumentNullException(nameof(credentialService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// /// Sincronizza le cancellazioni dalla sorgente alla destinazione /// public async Task SyncDeletionsAsync( List currentSourceKeyValues, string destinationEntity, string restCredentialName, IRestServiceClient restClient, DeletionSyncOptions options) { var result = new DeletionSyncResult { StartTime = DateTime.Now }; try { _logger.LogInformation("Inizio sincronizzazione cancellazioni per {Entity} - {Credential}", destinationEntity, restCredentialName); // Step 1: Marca le associazioni come cancellate se non sono più presenti nella sorgente var markedCount = await _credentialService.MarkDeletedAssociationsAsync( currentSourceKeyValues, destinationEntity, restCredentialName); result.DeletedRecordsDetected = markedCount; _logger.LogInformation("Rilevati {Count} record cancellati dalla sorgente", markedCount); if (markedCount == 0) { result.IsSuccess = true; result.Message = "Nessun record cancellato rilevato"; result.EndTime = DateTime.Now; return result; } // Step 2: Ottieni le cancellazioni in attesa di sincronizzazione var pendingDeletions = await _credentialService.GetPendingDeletionsAsync( destinationEntity, restCredentialName); _logger.LogInformation("Trovate {Count} cancellazioni in attesa di sincronizzazione", pendingDeletions.Count); // Step 3: Esegui le cancellazioni nella destinazione. // Per Salesforce usiamo le Composite API in batch (ceil(N/25) HTTP call invece di N); // per gli altri client REST manteniamo il loop sequenziale (nessun batch supportato). if (restClient is SalesforceServiceClient salesforceClient) { await ExecuteBatchedSalesforceDeletionsAsync( salesforceClient, destinationEntity, pendingDeletions, options, result); } else { await ExecuteSequentialDeletionsAsync( restClient, destinationEntity, pendingDeletions, options, result); } result.IsSuccess = result.SyncErrors == 0; result.Message = result.IsSuccess ? $"Sincronizzazione completata: {result.DeletedRecordsSynced} record cancellati" : $"Sincronizzazione completata con errori: {result.DeletedRecordsSynced} sincronizzati, {result.SyncErrors} errori"; _logger.LogInformation(result.Message); } catch (Exception ex) { result.IsSuccess = false; result.Message = $"Errore durante la sincronizzazione delle cancellazioni: {ex.Message}"; result.Errors.Add(ex.Message); _logger.LogError(ex, "Errore durante la sincronizzazione delle cancellazioni"); } result.EndTime = DateTime.Now; return result; } /// /// Esegue le cancellazioni in batch via Salesforce Composite API. /// Riduce N round-trip HTTP a ceil(N/25) batch in parallelo. /// private async Task ExecuteBatchedSalesforceDeletionsAsync( SalesforceServiceClient salesforceClient, string destinationEntity, List pendingDeletions, DeletionSyncOptions options, DeletionSyncResult result) { // Per Mark serve MarkField e MarkValue: validazione preventiva (un solo log) if (options.Action == DeletionAction.Mark && (string.IsNullOrEmpty(options.MarkField) || string.IsNullOrEmpty(options.MarkValue))) { const string err = "MarkField e MarkValue devono essere specificati per DeletionAction.Mark"; _logger.LogWarning(err); foreach (var d in pendingDeletions) { result.SyncErrors++; result.Errors.Add($"KeyValue: {d.KeyValue} - {err}"); } return; } // Mappa entityId → KeyAssociation per ricostruire l'associazione dal risultato batch var deletionsById = pendingDeletions .Where(d => !string.IsNullOrEmpty(d.DestinationId)) .GroupBy(d => d.DestinationId) .ToDictionary(g => g.Key, g => g.First()); // se duplicati, prima occorrenza var entityIds = deletionsById.Keys.ToList(); if (entityIds.Count == 0) return; _logger.LogInformation("DELETION SYNC (Salesforce batched): {Count} record, action={Action}", entityIds.Count, options.Action); List batchResults; try { switch (options.Action) { case DeletionAction.Delete: batchResults = await salesforceClient.BatchDeleteEntitiesAsync(destinationEntity, entityIds); break; case DeletionAction.Deactivate: // Aggiorna IsActive/Active = false in batch. // Non sappiamo a priori quale dei due campi esista sull'SObject: proviamo IsActive, // se Salesforce ritorna errore il record verrà segnalato come fallito. var deactivateUpdates = entityIds.ToDictionary( id => id, _ => (Dictionary)new Dictionary { { "IsActive", false } }); batchResults = await salesforceClient.BatchUpdateEntitiesAsync(destinationEntity, deactivateUpdates); break; case DeletionAction.Mark: batchResults = await salesforceClient.BatchPatchSingleFieldAsync( destinationEntity, entityIds, options.MarkField!, options.MarkValue!); break; default: _logger.LogWarning("DELETION SYNC: azione non supportata: {Action}", options.Action); foreach (var d in pendingDeletions) { result.SyncErrors++; result.Errors.Add($"KeyValue: {d.KeyValue} - Azione non supportata: {options.Action}"); } return; } } catch (Exception ex) { _logger.LogError(ex, "DELETION SYNC: errore nell'esecuzione del batch Salesforce"); foreach (var d in pendingDeletions) { result.SyncErrors++; result.Errors.Add($"KeyValue: {d.KeyValue} - {ex.Message}"); } return; } // Aggiorna lo stato delle cancellazioni in DB in parallelo per i record sincronizzati con successo var markSyncedTasks = new List(); foreach (var br in batchResults) { if (!deletionsById.TryGetValue(br.EntityId ?? string.Empty, out var deletion)) continue; if (br.Success) { result.DeletedRecordsSynced++; markSyncedTasks.Add(_credentialService.MarkDeletionSyncedAsync(deletion.Id)); _logger.LogDebug( "DELETION SYNC: KeyValue={KeyValue}, DestinationId={DestinationId}, Action={Action} OK", deletion.KeyValue, deletion.DestinationId, options.Action); } else { result.SyncErrors++; var msg = $"KeyValue: {deletion.KeyValue} - {br.ErrorMessage ?? "Unknown error"}"; result.Errors.Add(msg); _logger.LogWarning("DELETION SYNC fallita: {Msg}", msg); } } if (markSyncedTasks.Count > 0) await Task.WhenAll(markSyncedTasks); } /// /// Fallback sequenziale per client REST non Salesforce. /// private async Task ExecuteSequentialDeletionsAsync( IRestServiceClient restClient, string destinationEntity, List pendingDeletions, DeletionSyncOptions options, DeletionSyncResult result) { foreach (var deletion in pendingDeletions) { try { bool syncSuccess = false; string errorMessage = ""; switch (options.Action) { case DeletionAction.Delete: syncSuccess = await DeleteRecordAsync(restClient, destinationEntity, deletion.DestinationId); break; case DeletionAction.Deactivate: syncSuccess = await DeactivateRecordAsync(restClient, destinationEntity, deletion.DestinationId); break; case DeletionAction.Mark: if (string.IsNullOrEmpty(options.MarkField) || string.IsNullOrEmpty(options.MarkValue)) { errorMessage = "MarkField e MarkValue devono essere specificati per DeletionAction.Mark"; _logger.LogWarning(errorMessage); result.Errors.Add($"KeyValue: {deletion.KeyValue} - {errorMessage}"); continue; } syncSuccess = await MarkRecordAsync(restClient, destinationEntity, deletion.DestinationId, options.MarkField, options.MarkValue); break; default: errorMessage = $"Azione di cancellazione non supportata: {options.Action}"; _logger.LogWarning(errorMessage); result.Errors.Add($"KeyValue: {deletion.KeyValue} - {errorMessage}"); continue; } if (syncSuccess) { await _credentialService.MarkDeletionSyncedAsync(deletion.Id); result.DeletedRecordsSynced++; } else { result.SyncErrors++; result.Errors.Add($"Errore nella sincronizzazione della cancellazione per KeyValue: {deletion.KeyValue}"); } } catch (Exception ex) { result.SyncErrors++; result.Errors.Add($"KeyValue: {deletion.KeyValue} - {ex.Message}"); _logger.LogError(ex, "Errore nella sincronizzazione della cancellazione per {KeyValue}", deletion.KeyValue); } } } /// /// Elimina fisicamente un record dalla destinazione /// private async Task DeleteRecordAsync( IRestServiceClient restClient, string entityName, string entityId) { try { return await restClient.DeleteEntityAsync(entityName, entityId); } catch (Exception ex) { _logger.LogError(ex, "Errore nell'eliminazione del record {EntityId} dall'entità {Entity}", entityId, entityName); return false; } } /// /// Marca un record come inattivo nella destinazione /// private async Task DeactivateRecordAsync( IRestServiceClient restClient, string entityName, string entityId) { try { var updateData = new Dictionary { { "IsActive", false }, { "Active", false } // Prova entrambi i campi comuni }; var result = await restClient.UpdateEntityAsync(entityName, entityId, updateData); return result != null; } catch (Exception ex) { _logger.LogError(ex, "Errore nella disattivazione del record {EntityId} dall'entità {Entity}", entityId, entityName); return false; } } /// /// Imposta un campo personalizzato per marcare un record come cancellato /// private async Task MarkRecordAsync( IRestServiceClient restClient, string entityName, string entityId, string markField, string markValue) { try { var updateData = new Dictionary { { markField, markValue } }; var result = await restClient.UpdateEntityAsync(entityName, entityId, updateData); return result != null; } catch (Exception ex) { _logger.LogError(ex, "Errore nella marcatura del record {EntityId} dall'entità {Entity}", entityId, entityName); return false; } } } /// /// Opzioni per la sincronizzazione delle cancellazioni /// public class DeletionSyncOptions { public DeletionAction Action { get; set; } = DeletionAction.Delete; public string? MarkField { get; set; } public string? MarkValue { get; set; } } /// /// Azione da eseguire per i record cancellati /// public enum DeletionAction { /// /// Elimina fisicamente il record /// Delete, /// /// Marca il record come inattivo /// Deactivate, /// /// Imposta un campo personalizzato /// Mark } /// /// Risultato della sincronizzazione delle cancellazioni /// public class DeletionSyncResult { public bool IsSuccess { get; set; } public string Message { get; set; } = ""; public int DeletedRecordsDetected { get; set; } public int DeletedRecordsSynced { get; set; } public int SyncErrors { get; set; } public List Errors { get; set; } = new(); public DateTime StartTime { get; set; } public DateTime EndTime { get; set; } public TimeSpan Duration => EndTime - StartTime; }