using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SVSim.Database; using SVSim.Database.Enums; using SVSim.Database.Models; using SVSim.Database.Repositories.Pack; using SVSim.EmulatedEntrypoint.Models.Dtos; namespace SVSim.EmulatedEntrypoint.Services; public class CardAcquisitionService : ICardAcquisitionService { private readonly SVSimDbContext _db; private readonly IPackRepository _packs; private readonly ILogger _log; public CardAcquisitionService(SVSimDbContext db, IPackRepository packs, ILogger log) { _db = db; _packs = packs; _log = log; } public async Task GrantAsync(long viewerId, IEnumerable? newCardIds = null) { var rewardList = new List(); var viewer = await _db.Viewers .Include(v => v.Cards).ThenInclude(c => c.Card) .Include(v => v.LeaderSkins) .Include(v => v.Sleeves) .Include(v => v.Emblems) .Include(v => v.Degrees) .Include(v => v.MyPageBackgrounds) .AsSplitQuery() .FirstAsync(v => v.Id == viewerId); List lookupSourceCardIds; if (newCardIds is not null) { var newIds = newCardIds.ToList(); await _packs.GrantCardsToViewer(viewerId, newIds); // GrantCardsToViewer mutates OwnedCardEntry rows on the same scoped SVSimDbContext // AND commits them via its own SaveChangesAsync. The change tracker already exposes // the post-state via viewer.Cards. Note this means GrantAsync performs two saves total // (one inside the repo call, one at the end for cosmetic grants) — accepted because // any inconsistency in the failure window is self-healing via the next /load/index // backfill. lookupSourceCardIds = newIds.Distinct().ToList(); foreach (var distinctId in lookupSourceCardIds) { var owned = viewer.Cards.First(c => c.Card.Id == distinctId); rewardList.Add(new RewardListEntry { RewardType = 5, RewardId = distinctId, RewardNum = owned.Count }); } } else { // Backfill mode: scan all owned cards for missing cosmetics. No card-count mutation. lookupSourceCardIds = viewer.Cards.Select(c => c.Card.Id).Distinct().ToList(); } // Foil resolution: cosmetic mappings are recorded on the non-foil card row. // Foil twins (card_id + 1) inherit via the universal +1 convention. var lookupCardIds = lookupSourceCardIds .Select(id => { var card = viewer.Cards.FirstOrDefault(c => c.Card.Id == id)?.Card; return (card?.IsFoil == true) ? id - 1 : id; }) .Distinct() .ToList(); var rewards = await _db.CardCosmeticRewards .Where(r => lookupCardIds.Contains(r.CardId)) .ToListAsync(); foreach (var reward in rewards) { if (await TryGrant(viewer, reward)) { rewardList.Add(new RewardListEntry { RewardType = (int)reward.Type, RewardId = reward.CosmeticId, RewardNum = reward.Quantity, }); } } await _db.SaveChangesAsync(); return new CardGrantResult(rewardList); } /// /// Returns true if the cosmetic was newly granted (caller should emit a reward_list entry). /// Returns false if the viewer already owned it or the master row is missing (defensive log). /// private async Task TryGrant(Viewer viewer, CardCosmeticReward reward) { var id = (int)reward.CosmeticId; // master tables use int Id switch (reward.Type) { case CosmeticType.Skin: { if (viewer.LeaderSkins.Any(s => s.Id == id)) return false; var master = await _db.LeaderSkins.FindAsync(id); if (master is null) { _log.LogWarning("Skin master row missing for cosmetic_id={Id}", id); return false; } viewer.LeaderSkins.Add(master); return true; } case CosmeticType.Sleeve: { if (viewer.Sleeves.Any(s => s.Id == id)) return false; var master = await _db.Sleeves.FindAsync(id); if (master is null) { _log.LogWarning("Sleeve master row missing for cosmetic_id={Id}", id); return false; } viewer.Sleeves.Add(master); return true; } case CosmeticType.Emblem: { if (viewer.Emblems.Any(e => e.Id == id)) return false; var master = await _db.Emblems.FindAsync(id); if (master is null) { _log.LogWarning("Emblem master row missing for cosmetic_id={Id}", id); return false; } viewer.Emblems.Add(master); return true; } case CosmeticType.Degree: { if (viewer.Degrees.Any(d => d.Id == id)) return false; var master = await _db.Degrees.FindAsync(id); if (master is null) { _log.LogWarning("Degree master row missing for cosmetic_id={Id}", id); return false; } viewer.Degrees.Add(master); return true; } case CosmeticType.MyPageBG: { if (viewer.MyPageBackgrounds.Any(b => b.Id == id)) return false; var master = await _db.MyPageBackgrounds.FindAsync(id); if (master is null) { _log.LogWarning("MyPageBG master row missing for cosmetic_id={Id}", id); return false; } viewer.MyPageBackgrounds.Add(master); return true; } default: _log.LogWarning("Unknown CosmeticType {Type} for card {CardId}", reward.Type, reward.CardId); return false; } } }