using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using CredentialManager.Models; using CredentialManager.Services; using DataConnection.CredentialManagement.Interfaces; using DataConnection.REST.Interfaces; using Microsoft.Extensions.Logging; namespace Data_Coupler.Services; /// /// Servizio per la gestione delle associazioni tra record sorgente e destinazione. /// Include logica di Pre-Discovery per trovare record esistenti prima di creare duplicati. /// public class AssociationService : IAssociationService { private readonly IDataConnectionCredentialService _credentialService; private readonly ILogger _logger; public AssociationService( IDataConnectionCredentialService credentialService, ILogger logger) { _credentialService = credentialService ?? throw new ArgumentNullException(nameof(credentialService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// /// Trova o crea un'associazione tramite Pre-Discovery. /// Se non esiste un'associazione locale, cerca nella destinazione REST. /// Se trova un record, crea l'associazione automaticamente. /// /// Parametri per la ricerca/creazione associazione /// Associazione esistente o appena creata, null se non trovata public async Task FindOrCreateAssociationAsync(PreDiscoveryRequest request) { if (request == null) throw new ArgumentNullException(nameof(request)); if (string.IsNullOrEmpty(request.SourceKey)) { _logger.LogWarning("FindOrCreateAssociationAsync: SourceKey vuoto, skip"); return null; } // Step 1: Cerca associazione esistente var existingAssociation = await FindExistingAssociationAsync(request); if (existingAssociation != null) { _logger.LogDebug("Associazione esistente trovata: ID={AssociationId}, DestinationId={DestinationId}", existingAssociation.Id, existingAssociation.DestinationId); return existingAssociation; } // Step 2: Pre-Discovery nella destinazione REST if (request.EnablePreDiscovery) { var discoveredAssociation = await PerformPreDiscoveryAsync(request); if (discoveredAssociation != null) { return discoveredAssociation; } } // Non trovata né in locale né in destinazione return null; } /// /// Verifica se un'associazione è stata creata dal Pre-Discovery /// controllando il campo AdditionalInfo /// public bool IsPreDiscoveryAssociation(KeyAssociation association) { if (association == null || string.IsNullOrEmpty(association.AdditionalInfo)) return false; try { var additionalInfo = JsonSerializer.Deserialize>(association.AdditionalInfo); if (additionalInfo != null && additionalInfo.ContainsKey("CreatedBy")) { var createdBy = additionalInfo["CreatedBy"]?.ToString(); return createdBy == "PreDiscovery"; } } catch (Exception ex) { _logger.LogWarning(ex, "Errore parsing AdditionalInfo per AssociationId={Id}", association.Id); } return false; } #region Private Methods /// /// Cerca un'associazione esistente nel database locale /// private async Task FindExistingAssociationAsync(PreDiscoveryRequest request) { _logger.LogDebug("Cerco associazione per KeyValue='{KeyValue}', Entity='{Entity}', Credential='{Credential}'", request.SourceKey, request.DestinationEntity, request.CredentialName); // Cerca con tutti i parametri var association = request.UseParallelMethod ? await _credentialService.FindKeyAssociationByValueParallelAsync( request.SourceKey, request.DestinationEntity, request.CredentialName) : await _credentialService.FindKeyAssociationByValueAsync( request.SourceKey, request.DestinationEntity, request.CredentialName); // FALLBACK: Se non trovata, cerca solo per KeyValue if (association == null) { _logger.LogDebug("Associazione non trovata con parametri specifici, provo solo con KeyValue"); association = request.UseParallelMethod ? await _credentialService.FindKeyAssociationByValueParallelAsync(request.SourceKey) : await _credentialService.FindKeyAssociationByValueAsync(request.SourceKey); if (association != null) { // Verifica compatibilità if (association.DestinationEntity != request.DestinationEntity || association.RestCredentialName != request.CredentialName) { _logger.LogDebug("Associazione non compatibile: Entity={FoundEntity} vs {ExpectedEntity}, Credential={FoundCred} vs {ExpectedCred}", association.DestinationEntity, request.DestinationEntity, association.RestCredentialName, request.CredentialName); association = null; } } } return association; } /// /// Esegue il Pre-Discovery cercando nella destinazione REST /// private async Task PerformPreDiscoveryAsync(PreDiscoveryRequest request) { _logger.LogInformation("PRE-DISCOVERY: Nessuna associazione trovata per '{KeyValue}'. Cerco nella destinazione...", request.SourceKey); // Verifica che il campo chiave sia mappato if (!request.FieldMappings.TryGetValue(request.SourceKeyField, out var mappedDestinationFieldName)) { _logger.LogWarning("PRE-DISCOVERY: Campo chiave '{SourceKeyField}' non trovato nei mappings. Skip discovery.", request.SourceKeyField); return null; } try { // Prepara i campi di ricerca var searchFields = new Dictionary { { mappedDestinationFieldName, request.SourceKey } }; _logger.LogInformation("PRE-DISCOVERY: Cerco in '{Entity}' dove {Field} = '{Value}'", request.DestinationEntity, mappedDestinationFieldName, request.SourceKey); // Cerca nella destinazione REST var existingEntities = await request.RestClient.FindEntitiesByKeysAsync( request.DestinationEntity, searchFields); if (existingEntities == null || existingEntities.Count == 0) { _logger.LogInformation("PRE-DISCOVERY: Nessun record esistente trovato per KeyValue: '{KeyValue}'", request.SourceKey); return null; } // Trovato! Prendi il primo risultato var foundEntity = existingEntities[0]; // Estrai l'ID del record trovato var destinationId = ExtractDestinationId(foundEntity); if (string.IsNullOrEmpty(destinationId)) { _logger.LogWarning("PRE-DISCOVERY: Record trovato ma senza ID valido per KeyValue: '{KeyValue}'", request.SourceKey); return null; } _logger.LogInformation("PRE-DISCOVERY: ✅ Trovato record esistente! KeyValue: '{KeyValue}' -> DestinationId: '{DestinationId}'", request.SourceKey, destinationId); // Crea l'associazione var newAssociation = CreatePreDiscoveryAssociation( request, mappedDestinationFieldName, destinationId); // Salva l'associazione var associationId = request.UseParallelMethod ? await _credentialService.SaveKeyAssociationParallelAsync(newAssociation) : await _credentialService.SaveKeyAssociationAsync(newAssociation); _logger.LogInformation("PRE-DISCOVERY: Associazione creata con ID: {AssociationId}", associationId); newAssociation.Id = associationId; return newAssociation; } catch (Exception ex) { _logger.LogWarning(ex, "PRE-DISCOVERY: Errore durante la ricerca nella destinazione per KeyValue: '{KeyValue}'", request.SourceKey); return null; } } /// /// Estrae l'ID di destinazione dal record trovato /// private string? ExtractDestinationId(Dictionary entity) { // Prova "Id" (case-sensitive) if (entity.ContainsKey("Id")) return entity["Id"]?.ToString(); // Prova "id" (lowercase) if (entity.ContainsKey("id")) return entity["id"]?.ToString(); // Prova case-insensitive var idKey = entity.Keys.FirstOrDefault(k => k.Equals("Id", StringComparison.OrdinalIgnoreCase)); return idKey != null ? entity[idKey]?.ToString() : null; } /// /// Crea una nuova associazione con marker Pre-Discovery /// private KeyAssociation CreatePreDiscoveryAssociation( PreDiscoveryRequest request, string mappedDestinationFieldName, string destinationId) { var additionalInfo = new Dictionary { { "CreatedBy", "PreDiscovery" }, { "DiscoveredAt", DateTime.UtcNow }, { "MappingCount", request.FieldMappings.Count } }; // Aggiungi info su scheduled transfer se specificato if (request.IsScheduledTransfer) { additionalInfo.Add("ScheduledTransfer", true); } // Aggiungi source type se specificato if (!string.IsNullOrEmpty(request.SourceType)) { additionalInfo.Add("SourceType", request.SourceType); } return new KeyAssociation { KeyValue = request.SourceKey, SourceKeyField = request.SourceKeyField, DestinationKeyField = request.DestinationKeyField ?? "Id", MappedDestinationField = mappedDestinationFieldName, DestinationEntity = request.DestinationEntity, DestinationId = destinationId, RestCredentialName = request.CredentialName, CreatedAt = DateTime.UtcNow, LastVerifiedAt = DateTime.UtcNow, IsActive = true, Data_Hash = request.CurrentDataHash, AdditionalInfo = JsonSerializer.Serialize(additionalInfo) }; } #endregion } /// /// Interfaccia per il servizio di gestione associazioni /// public interface IAssociationService { Task FindOrCreateAssociationAsync(PreDiscoveryRequest request); bool IsPreDiscoveryAssociation(KeyAssociation association); } /// /// Parametri per la ricerca/creazione di associazioni con Pre-Discovery /// public class PreDiscoveryRequest { /// /// Valore della chiave sorgente (es: "C00001") /// public string SourceKey { get; set; } = string.Empty; /// /// Nome del campo chiave nella sorgente (es: "CardCode") /// public string SourceKeyField { get; set; } = string.Empty; /// /// Nome dell'entità destinazione (es: "Account") /// public string DestinationEntity { get; set; } = string.Empty; /// /// Nome della credenziale REST (es: "Salesforce_Prod") /// public string CredentialName { get; set; } = string.Empty; /// /// Campo ID nella destinazione (default: "Id") /// public string? DestinationKeyField { get; set; } /// /// Mappings campo sorgente -> campo destinazione /// public Dictionary FieldMappings { get; set; } = new(); /// /// Client REST per effettuare la ricerca nella destinazione /// public IRestServiceClient RestClient { get; set; } = null!; /// /// Hash dei dati correnti da salvare nell'associazione /// public string? CurrentDataHash { get; set; } /// /// Se abilitare il Pre-Discovery (default: true) /// public bool EnablePreDiscovery { get; set; } = true; /// /// Se usare i metodi paralleli thread-safe (default: false) /// public bool UseParallelMethod { get; set; } = false; /// /// Se la richiesta proviene da un trasferimento schedulato /// public bool IsScheduledTransfer { get; set; } = false; /// /// Tipo di sorgente (database, file, etc.) /// public string? SourceType { get; set; } }