using System.Text.Json; using Microsoft.EntityFrameworkCore; using SVSim.Bootstrap.Models.Seed; using SVSim.Database; using SVSim.Database.Models; namespace SVSim.Bootstrap.Importers; /// /// Idempotent upsert of the six card-id-keyed tables from load-index seeds: /// SpotCards, ReprintedCards, UnlimitedRestrictions, LoadingExclusionCards, /// MaintenanceCards, FeatureMaintenances. Loads the Cards FK set once for orphan warnings. /// Rows missing from a seed are LEFT INTACT — a partial seed shouldn't silently delete entries. /// FeatureMaintenances clears-and-rewrites because its synthetic ordinal Id has no natural-key semantics. /// public class CardListsImporter { public async Task ImportAsync(SVSimDbContext context, string seedDir) { var knownCards = new HashSet(await context.Cards.Select(c => c.Id).ToListAsync()); int total = 0; total += await ImportSpot(context, seedDir, knownCards); total += await ImportReprinted(context, seedDir, knownCards); total += await ImportUnlimited(context, seedDir, knownCards); total += await ImportLoadingExclusion(context, seedDir, knownCards); total += await ImportMaintenance(context, seedDir); total += await ImportFeatureMaintenances(context, seedDir); await context.SaveChangesAsync(); return total; } private async Task ImportSpot(SVSimDbContext context, string seedDir, HashSet knownCards) { var seed = SeedLoader.LoadList(Path.Combine(seedDir, "spot-cards.json")); if (seed.Count == 0) return 0; var existing = await context.SpotCards.ToDictionaryAsync(e => e.Id); int created = 0, updated = 0, orphans = 0; foreach (var s in seed) { if (s.CardId == 0) continue; if (!knownCards.Contains(s.CardId)) orphans++; var entry = existing.TryGetValue(s.CardId, out var ex) ? ex : new SpotCardEntry { Id = s.CardId }; entry.Cost = s.Cost; if (ex is null) { context.SpotCards.Add(entry); existing[s.CardId] = entry; created++; } else updated++; } WarnOrphans("SpotCards", orphans); Console.WriteLine($"[CardListsImporter] SpotCards +{created}/~{updated}"); return created + updated; } private async Task ImportReprinted(SVSimDbContext context, string seedDir, HashSet knownCards) { var seed = SeedLoader.LoadList(Path.Combine(seedDir, "reprinted-cards.json")); if (seed.Count == 0) return 0; var existing = await context.ReprintedCards.ToDictionaryAsync(e => e.Id); int created = 0, orphans = 0; foreach (var s in seed) { if (s.CardId == 0) continue; if (!knownCards.Contains(s.CardId)) orphans++; if (existing.ContainsKey(s.CardId)) continue; var entry = new ReprintedCardEntry { Id = s.CardId }; context.ReprintedCards.Add(entry); existing[s.CardId] = entry; created++; } WarnOrphans("ReprintedCards", orphans); Console.WriteLine($"[CardListsImporter] ReprintedCards +{created}"); return created; } private async Task ImportUnlimited(SVSimDbContext context, string seedDir, HashSet knownCards) { var seed = SeedLoader.LoadList(Path.Combine(seedDir, "unlimited-restrictions.json")); if (seed.Count == 0) return 0; var existing = await context.UnlimitedRestrictions.ToDictionaryAsync(e => e.Id); int created = 0, updated = 0, orphans = 0; foreach (var s in seed) { if (s.CardId == 0) continue; if (!knownCards.Contains(s.CardId)) orphans++; var entry = existing.TryGetValue(s.CardId, out var ex) ? ex : new UnlimitedRestrictionEntry { Id = s.CardId }; entry.RestrictionValue = s.RestrictionValue; if (ex is null) { context.UnlimitedRestrictions.Add(entry); existing[s.CardId] = entry; created++; } else updated++; } WarnOrphans("UnlimitedRestrictions", orphans); Console.WriteLine($"[CardListsImporter] UnlimitedRestrictions +{created}/~{updated}"); return created + updated; } private async Task ImportLoadingExclusion(SVSimDbContext context, string seedDir, HashSet knownCards) { var seed = SeedLoader.LoadList(Path.Combine(seedDir, "loading-exclusion-cards.json")); if (seed.Count == 0) return 0; var existing = await context.LoadingExclusionCards.ToDictionaryAsync(e => e.Id); int created = 0, orphans = 0; foreach (var s in seed) { if (s.CardId == 0) continue; if (!knownCards.Contains(s.CardId)) orphans++; if (existing.ContainsKey(s.CardId)) continue; var entry = new LoadingExclusionCardEntry { Id = s.CardId }; context.LoadingExclusionCards.Add(entry); existing[s.CardId] = entry; created++; } WarnOrphans("LoadingExclusionCards", orphans); Console.WriteLine($"[CardListsImporter] LoadingExclusionCards +{created}"); return created; } private async Task ImportMaintenance(SVSimDbContext context, string seedDir) { var seed = SeedLoader.LoadList(Path.Combine(seedDir, "maintenance-cards.json")); if (seed.Count == 0) return 0; var existing = await context.MaintenanceCards.ToDictionaryAsync(e => e.Id); int created = 0; foreach (var s in seed) { if (s.CardId == 0) continue; if (existing.ContainsKey(s.CardId)) continue; var entry = new MaintenanceCardEntry { Id = s.CardId }; context.MaintenanceCards.Add(entry); existing[s.CardId] = entry; created++; } Console.WriteLine($"[CardListsImporter] MaintenanceCards +{created}"); return created; } private async Task ImportFeatureMaintenances(SVSimDbContext context, string seedDir) { var seed = SeedLoader.LoadList(Path.Combine(seedDir, "feature-maintenances.json")); if (seed.Count == 0) return 0; // FeatureMaintenances has a synthetic int Id assigned by the extractor (1-based ordinal). // FeatureMaintenances use a synthetic ordinal id from the extractor; we clear-and-rewrite to // keep re-runs idempotent and match "the latest seed is authoritative". Pre-existing rows // with seed-absent ids are dropped here (acceptable: only synthetic ordinals, no FKs // reference this table). var existing = await context.FeatureMaintenances.ToListAsync(); context.FeatureMaintenances.RemoveRange(existing); int created = 0; foreach (var s in seed) { if (s.Id == 0) continue; context.FeatureMaintenances.Add(new FeatureMaintenanceEntry { Id = s.Id, FeatureKey = s.FeatureKey, Data = s.Data.ValueKind == JsonValueKind.Undefined ? "{}" : JsonSerializer.Serialize(s.Data), }); created++; } Console.WriteLine($"[CardListsImporter] FeatureMaintenances: -{existing.Count}/+{created}"); return created; } private static void WarnOrphans(string label, int count) { if (count > 0) Console.Error.WriteLine($"[CardListsImporter] Warning: {label} has {count} orphan card_id(s) — run CardImporter first for clean references."); } }