using System.Text.Json; using System.Text.Json.Serialization; using GerbilManagerWebAPI.Models; using Microsoft.EntityFrameworkCore; namespace GerbilManagerWebAPI.Import { public sealed class IngestResolvedService { private readonly ApplicationContext _db; private readonly string _resolvedJsonPath; private readonly string _sourceDir; private readonly string _photoRoot; public IngestResolvedService(ApplicationContext db, IConfiguration config, IWebHostEnvironment? env) { _db = db; var contentRoot = env?.ContentRootPath ?? Directory.GetCurrentDirectory(); _sourceDir = config["Import:SourcePath"] ?? Path.GetFullPath(Path.Combine(contentRoot, "..", "tools", "import", "output")); _resolvedJsonPath = Path.Combine(_sourceDir, "resolved_import.json"); _photoRoot = config["Photos:RootPath"] ?? Path.Combine(contentRoot, "photo-storage"); } public async Task RunAsync() { if (!File.Exists(_resolvedJsonPath)) { return $"Error: Resolved import file not found at {_resolvedJsonPath}. Run resolve_import.py first."; } string jsonContent = await File.ReadAllTextAsync(_resolvedJsonPath); var jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; jsonOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); var data = JsonSerializer.Deserialize(jsonContent, jsonOptions); if (data == null) { return "Error: Failed to deserialize resolved_import.json."; } // 1. Provider-agnostic clean wipe of dependent tables to prevent duplicate keys if (_db.Database.ProviderName == "Microsoft.EntityFrameworkCore.InMemory") { // Break circular references first foreach (var l in _db.Litters) { l.FatherId = null; l.MotherId = null; } foreach (var g in _db.Gerbils) { g.LitterId = null; } await _db.SaveChangesAsync(); _db.GerbilPhotos.RemoveRange(_db.GerbilPhotos); _db.WeightRecords.RemoveRange(_db.WeightRecords); _db.HealthRecords.RemoveRange(_db.HealthRecords); _db.Gerbils.RemoveRange(_db.Gerbils); _db.Litters.RemoveRange(_db.Litters); _db.Contacts.RemoveRange(_db.Contacts); await _db.SaveChangesAsync(); } else { // Break circular references first using ExecuteUpdateAsync await _db.Litters.ExecuteUpdateAsync(s => s .SetProperty(l => l.FatherId, (Guid?)null) .SetProperty(l => l.MotherId, (Guid?)null)); await _db.Gerbils.ExecuteUpdateAsync(s => s .SetProperty(g => g.LitterId, (Guid?)null)); await _db.GerbilPhotos.ExecuteDeleteAsync(); await _db.WeightRecords.ExecuteDeleteAsync(); await _db.HealthRecords.ExecuteDeleteAsync(); await _db.Gerbils.ExecuteDeleteAsync(); await _db.Litters.ExecuteDeleteAsync(); await _db.Contacts.ExecuteDeleteAsync(); } // 2. Import Contacts (Add new ones, and update roles/details of existing ones) var existingContacts = await _db.Contacts.ToDictionaryAsync(c => c.Id); var addedContactIds = new HashSet(); int contactsAdded = 0; int contactsUpdated = 0; foreach (var c in data.Contacts) { if (existingContacts.TryGetValue(c.Id, out var existingContact)) { existingContact.Name = c.Name; existingContact.Email = c.Email; existingContact.Phone = c.Phone; existingContact.Address = c.Address; existingContact.Notes = c.Notes; existingContact.IsBreeder = c.IsBreeder; existingContact.IsReceiver = c.IsReceiver; contactsUpdated++; } else if (addedContactIds.Add(c.Id)) { _db.Contacts.Add(c); contactsAdded++; } } await _db.SaveChangesAsync(); // 3. Pass 1: Insert all Litters with parent references set to null to avoid FK errors var littersToInsert = new List(); foreach (var l in data.Litters) { littersToInsert.Add(new Litter { Id = l.Id, Name = l.Name, Date = l.Date, TotalBorn = l.TotalBorn, DeathsWithin8Weeks = l.DeathsWithin8Weeks, FatherId = null, // Set in Pass 2 MotherId = null, // Set in Pass 2 ExpectedGoHomeDate = l.ExpectedGoHomeDate, Notes = l.Notes, PairingCode = l.PairingCode, ExternalRef = l.ExternalRef, LitterLetter = l.LitterLetter }); } _db.Litters.AddRange(littersToInsert); await _db.SaveChangesAsync(); // 4. Pass 1: Insert all Gerbils with LitterId set to null to avoid FK errors var gerbilsToInsert = new List(); foreach (var g in data.Gerbils) { gerbilsToInsert.Add(new Gerbil { Id = g.Id, Name = g.Name, Gender = g.Gender, Status = g.Status, LitterId = null, // Set in Pass 2 OriginContactId = g.OriginContactId, ReceiverContactId = g.ReceiverContactId, EnclosureId = g.EnclosureId, ColorVarietyId = g.ColorVarietyId, DateOfBirth = g.DateOfBirth, DateOfDeath = g.DateOfDeath, CauseOfDeath = g.CauseOfDeath, GoHomeDate = g.GoHomeDate, Genotype = g.Genotype, Notes = g.Notes, ImportSource = g.ImportSource, ExternalRef = g.ExternalRef, RawImportData = g.RawImportData, OriginBreeder = g.OriginBreeder, NameSearch = g.NameSearch, CharacterTraits = g.CharacterTraits, CharacterNote = g.CharacterNote, IsDeaf = g.IsDeaf, IsResident = g.IsResident }); } _db.Gerbils.AddRange(gerbilsToInsert); await _db.SaveChangesAsync(); // 5. Pass 2: Set the actual foreign key relationships // Update Litters with their parent animal IDs var littersInDb = await _db.Litters.ToDictionaryAsync(l => l.Id); foreach (var l in data.Litters) { if (littersInDb.TryGetValue(l.Id, out var dbLitter)) { dbLitter.FatherId = l.FatherId; dbLitter.MotherId = l.MotherId; } } // Update Gerbils with their actual LitterId var gerbilsInDb = await _db.Gerbils.ToDictionaryAsync(g => g.Id); foreach (var g in data.Gerbils) { if (gerbilsInDb.TryGetValue(g.Id, out var dbGerbil)) { dbGerbil.LitterId = g.LitterId; } } await _db.SaveChangesAsync(); // 6. Insert all Photos & copy physical files Directory.CreateDirectory(_photoRoot); foreach (var p in data.GerbilPhotos) { if (!string.IsNullOrEmpty(p.SourcePath)) { var relPath = p.SourcePath.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar); var src = Path.Combine(_sourceDir, relPath); if (File.Exists(src)) { var dest = Path.Combine(_photoRoot, p.FileName); try { File.Copy(src, dest, overwrite: true); } catch (Exception ex) { Console.WriteLine($"Warning: Failed to copy photo {src} to {dest}: {ex.Message}"); } } else { Console.WriteLine($"Warning: Source photo file not found at {src}"); } } _db.GerbilPhotos.Add(new GerbilPhoto { Id = p.Id, GerbilId = p.GerbilId, FileName = p.FileName, Caption = p.Caption, SortOrder = p.SortOrder, CreatedAt = DateTimeOffset.UtcNow }); } await _db.SaveChangesAsync(); return $"Ingestion successful! Imported {contactsAdded} contacts ({contactsUpdated} updated), {data.Litters.Count} litters, {data.Gerbils.Count} gerbils, and {data.GerbilPhotos.Count} photos."; } } public class ResolvedImportData { public List Contacts { get; set; } = new(); public List Litters { get; set; } = new(); public List Gerbils { get; set; } = new(); public List GerbilPhotos { get; set; } = new(); } public class ResolvedPhoto { public Guid Id { get; set; } public Guid GerbilId { get; set; } public required string FileName { get; set; } public string? Caption { get; set; } public int SortOrder { get; set; } [JsonPropertyName("_source_path")] public string? SourcePath { get; set; } } }