using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using StarfaceOutlookSync.Models; namespace StarfaceOutlookSync.Services { public class SyncEngine { private readonly ProfileManager _profileManager = new ProfileManager(); private readonly OutlookContactsService _outlookService = new OutlookContactsService(); public event Action OnProgress; private void Log(string message) => OnProgress?.Invoke(message); /// /// Findet einen passenden Kontakt in der Kandidatenliste. /// Matching-Reihenfolge: E-Mail, dann Vorname+Nachname+Firma, dann Vorname+Nachname. /// private static UnifiedContact FindMatch(UnifiedContact contact, List candidates) { if (candidates == null || candidates.Count == 0) return null; // 1. Exakte E-Mail if (!string.IsNullOrEmpty(contact.Email)) { var byEmail = candidates.FirstOrDefault(c => !string.IsNullOrEmpty(c.Email) && c.Email.Equals(contact.Email, StringComparison.OrdinalIgnoreCase)); if (byEmail != null) return byEmail; } // 2. Vorname + Nachname + Firma (staerkstes Match ohne E-Mail) if ((!string.IsNullOrEmpty(contact.FirstName) || !string.IsNullOrEmpty(contact.LastName)) && !string.IsNullOrEmpty(contact.Company)) { var byNameCompany = candidates.FirstOrDefault(c => c.FirstName.Equals(contact.FirstName, StringComparison.OrdinalIgnoreCase) && c.LastName.Equals(contact.LastName, StringComparison.OrdinalIgnoreCase) && c.Company.Equals(contact.Company, StringComparison.OrdinalIgnoreCase)); if (byNameCompany != null) return byNameCompany; } // 3. Vorname + Nachname (ohne Firma) if (!string.IsNullOrEmpty(contact.FirstName) || !string.IsNullOrEmpty(contact.LastName)) { var byName = candidates.FirstOrDefault(c => c.FirstName.Equals(contact.FirstName, StringComparison.OrdinalIgnoreCase) && c.LastName.Equals(contact.LastName, StringComparison.OrdinalIgnoreCase) && (!string.IsNullOrEmpty(c.FirstName) || !string.IsNullOrEmpty(c.LastName))); if (byName != null) return byName; } // 4. Telefonnummer (Buero oder Mobil) if (!string.IsNullOrEmpty(contact.PhoneWork)) { var byPhone = candidates.FirstOrDefault(c => !string.IsNullOrEmpty(c.PhoneWork) && NormalizePhone(c.PhoneWork) == NormalizePhone(contact.PhoneWork)); if (byPhone != null) return byPhone; } if (!string.IsNullOrEmpty(contact.PhoneMobile)) { var byMobile = candidates.FirstOrDefault(c => !string.IsNullOrEmpty(c.PhoneMobile) && NormalizePhone(c.PhoneMobile) == NormalizePhone(contact.PhoneMobile)); if (byMobile != null) return byMobile; } return null; } private static string NormalizePhone(string phone) { if (string.IsNullOrEmpty(phone)) return ""; // Nur Ziffern und + behalten return new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray()); } public async Task SyncProfileAsync(SyncProfile profile) { var result = new SyncResult { ProfileName = profile.Name, Timestamp = DateTime.Now.ToString("o") }; try { Log("Verbinde mit Starface..."); using (var starface = new StarfaceApiClient(profile.StarfaceConnection)) { starface.OnDebug += (msg) => Log(msg); var loginOk = await starface.LoginAsync(); if (!loginOk) { result.ErrorMessages.Add("Starface-Login fehlgeschlagen"); result.Errors++; return result; } // Kontakte laden Log("Lade Outlook-Kontakte..."); var outlookContacts = _outlookService.GetContacts(profile.OutlookFolderPath); Log($"{outlookContacts.Count} Outlook-Kontakte geladen"); Log("Lade Starface-Kontakte..."); var starfaceContacts = await starface.GetContactsAsync(profile.StarfaceAddressBook); Log($"{starfaceContacts.Count} Starface-Kontakte geladen"); // Bestehende Mappings laden var mappings = _profileManager.GetMappings(profile.Id); // Sets fuer schnellen Lookup var mappingByOutlook = new Dictionary(); var mappingByStarface = new Dictionary(); foreach (var m in mappings) { if (!string.IsNullOrEmpty(m.OutlookEntryId)) mappingByOutlook[m.OutlookEntryId] = m; if (!string.IsNullOrEmpty(m.StarfaceId)) mappingByStarface[m.StarfaceId] = m; } // Tracking: welche Kontakte wurden bereits verarbeitet var processedStarfaceIds = new HashSet(); var processedOutlookIds = new HashSet(); var newMappings = new List(); // ============================================ // Phase 1: Bestehende Mappings abgleichen // ============================================ Log("Gleiche bestehende Zuordnungen ab..."); foreach (var mapping in mappings.ToList()) { var oc = outlookContacts.FirstOrDefault(c => c.OutlookEntryId == mapping.OutlookEntryId); var sc = starfaceContacts.FirstOrDefault(c => c.StarfaceId == mapping.StarfaceId); if (oc != null) processedOutlookIds.Add(oc.OutlookEntryId); if (sc != null) processedStarfaceIds.Add(sc.StarfaceId); if (oc == null && sc == null) { // Beide Seiten geloescht -> Mapping entfernen continue; } if (oc != null && sc != null) { // Beide vorhanden -> auf Aenderungen pruefen var olHash = oc.GetHash(); var sfHash = sc.GetHash(); bool olChanged = olHash != mapping.LastSyncHash; bool sfChanged = sfHash != mapping.LastSyncHash; if (olChanged && !sfChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface)) { // Outlook hat sich geaendert -> Starface updaten if (await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook)) { mapping.LastSyncHash = olHash; result.Updated++; Log($" Aktualisiert (OL->SF): {oc.DisplayName}"); } } else if (sfChanged && !olChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.StarfaceToOutlook)) { // Starface hat sich geaendert -> Outlook updaten if (_outlookService.UpdateContact(mapping.OutlookEntryId, sc)) { mapping.LastSyncHash = sfHash; result.Updated++; Log($" Aktualisiert (SF->OL): {sc.DisplayName}"); } } else if (olChanged && sfChanged) { // Beide geaendert -> Konflikt, neuere gewinnt (Outlook bevorzugt) if (profile.SyncDirection != SyncDirection.StarfaceToOutlook) { if (await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook)) { mapping.LastSyncHash = olHash; result.Updated++; Log($" Konflikt (OL gewinnt): {oc.DisplayName}"); } } else { if (_outlookService.UpdateContact(mapping.OutlookEntryId, sc)) { mapping.LastSyncHash = sfHash; result.Updated++; Log($" Konflikt (SF gewinnt): {sc.DisplayName}"); } } } // Beide unveraendert -> nichts tun } newMappings.Add(mapping); } // ============================================ // Phase 2: Neue Outlook-Kontakte (ohne Mapping) // ============================================ if (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface) { var unmappedOutlook = outlookContacts .Where(c => !string.IsNullOrEmpty(c.OutlookEntryId) && !processedOutlookIds.Contains(c.OutlookEntryId)) .ToList(); if (unmappedOutlook.Count > 0) Log($"Neue Outlook-Kontakte: {unmappedOutlook.Count}"); // Starface-Kontakte die noch kein Mapping haben (fuer Duplikat-Check) var unmappedStarface = starfaceContacts .Where(c => !string.IsNullOrEmpty(c.StarfaceId) && !processedStarfaceIds.Contains(c.StarfaceId)) .ToList(); foreach (var oc in unmappedOutlook) { try { // Duplikat-Check: existiert der Kontakt schon in der Starface? var match = FindMatch(oc, unmappedStarface); if (match != null) { // Existiert schon -> verknuepfen und updaten if (await starface.UpdateContactAsync(match.StarfaceId, oc, profile.StarfaceAddressBook)) { newMappings.Add(new SyncMapping { ProfileId = profile.Id, OutlookEntryId = oc.OutlookEntryId, StarfaceId = match.StarfaceId, LastSyncHash = oc.GetHash() }); processedStarfaceIds.Add(match.StarfaceId); unmappedStarface.Remove(match); result.Updated++; Log($" Verknuepft (OL->SF): {oc.DisplayName}"); } } else { // Neu -> in Starface erstellen Log($" Erstelle in Starface: {oc.DisplayName}"); var created = await starface.CreateContactAsync(oc, profile.StarfaceAddressBook); if (created != null && !string.IsNullOrEmpty(created.StarfaceId)) { newMappings.Add(new SyncMapping { ProfileId = profile.Id, OutlookEntryId = oc.OutlookEntryId, StarfaceId = created.StarfaceId, LastSyncHash = oc.GetHash() }); result.Created++; Log($" Erstellt (OL->SF): {oc.DisplayName}"); } else { Log($" FEHLER: Kontakt konnte nicht erstellt werden: {oc.DisplayName}"); result.Errors++; } } } catch (Exception ex) { result.Errors++; result.ErrorMessages.Add($"OL->SF {oc.DisplayName}: {ex.Message}"); } } } // ============================================ // Phase 3: Neue Starface-Kontakte (ohne Mapping) // ============================================ if (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.StarfaceToOutlook) { var unmappedStarface = starfaceContacts .Where(c => !string.IsNullOrEmpty(c.StarfaceId) && !processedStarfaceIds.Contains(c.StarfaceId)) .ToList(); if (unmappedStarface.Count > 0) Log($"Neue Starface-Kontakte: {unmappedStarface.Count}"); // Outlook-Kontakte die noch kein Mapping haben (fuer Duplikat-Check) var unmappedOutlook = outlookContacts .Where(c => !string.IsNullOrEmpty(c.OutlookEntryId) && !processedOutlookIds.Contains(c.OutlookEntryId)) .ToList(); foreach (var sc in unmappedStarface) { try { // Duplikat-Check: existiert der Kontakt schon in Outlook? var match = FindMatch(sc, unmappedOutlook); if (match != null) { // Existiert schon -> verknuepfen und updaten if (_outlookService.UpdateContact(match.OutlookEntryId, sc)) { newMappings.Add(new SyncMapping { ProfileId = profile.Id, OutlookEntryId = match.OutlookEntryId, StarfaceId = sc.StarfaceId, LastSyncHash = sc.GetHash() }); processedOutlookIds.Add(match.OutlookEntryId); unmappedOutlook.Remove(match); result.Updated++; Log($" Verknuepft (SF->OL): {sc.DisplayName}"); } } else { // Neu -> in Outlook erstellen var created = _outlookService.CreateContact(sc, profile.OutlookFolderPath); if (created != null && !string.IsNullOrEmpty(created.OutlookEntryId)) { newMappings.Add(new SyncMapping { ProfileId = profile.Id, OutlookEntryId = created.OutlookEntryId, StarfaceId = sc.StarfaceId, LastSyncHash = sc.GetHash() }); result.Created++; Log($" Erstellt (SF->OL): {sc.DisplayName}"); } } } catch (Exception ex) { result.Errors++; result.ErrorMessages.Add($"SF->OL {sc.DisplayName}: {ex.Message}"); } } } // Mappings speichern _profileManager.SaveMappings(profile.Id, newMappings); _profileManager.UpdateLastSync(profile.Id); await starface.LogoutAsync(); Log($"Fertig: {result.Created} erstellt, {result.Updated} aktualisiert, {result.Errors} Fehler"); } } catch (Exception ex) { result.Errors++; result.ErrorMessages.Add($"Allgemeiner Fehler: {ex.Message}"); } return result; } } }