using Marathon.Application.Abstractions; using Marathon.Application.Storage; using Marathon.Domain.Entities; using Marathon.Domain.ValueObjects; using Microsoft.EntityFrameworkCore; namespace Marathon.Infrastructure.Persistence.Repositories; internal sealed class EventRepository : IEventRepository { private readonly MarathonDbContext _db; public EventRepository(MarathonDbContext db) => _db = db; public async Task GetAsync(EventId key, CancellationToken ct = default) { var entity = await _db.Events.FindAsync([key.Value], ct); return entity is null ? null : Mapping.ToDomain(entity); } public async Task> ListAsync(CancellationToken ct = default) { var entities = await _db.Events.AsNoTracking().ToListAsync(ct); return entities.Select(Mapping.ToDomain).ToList().AsReadOnly(); } public async Task> ListByDateRangeAsync(DateRange range, CancellationToken ct = default) { // ScheduledAt is stored as ISO 8601 TEXT (see SqliteDateText); SQLite TEXT // comparison sorts chronologically for the fixed-offset O format. var fromStr = SqliteDateText.Key(range.From); var toStr = SqliteDateText.Key(range.To); // EF Core SQLite cannot translate string.Compare(...) with StringComparison; it can // translate the relational operators on string columns (which use BINARY/ordinal // collation by default in SQLite — correct for ISO 8601). var entities = await _db.Events.AsNoTracking() .Where(e => e.ScheduledAt.CompareTo(fromStr) >= 0 && e.ScheduledAt.CompareTo(toStr) <= 0) .ToListAsync(ct); return entities.Select(Mapping.ToDomain).ToList().AsReadOnly(); } public async Task> QueryAsync(EventQuery query, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(query); var fromStr = SqliteDateText.Key(query.Dates.From); var toStr = SqliteDateText.Key(query.Dates.To); // Date range + sport filter pushed to SQL so a multi-sport page no longer // materialises every event in the window. The composite // IX_Events_SportCode_ScheduledAt index covers this predicate. Case-sensitive // search / country filtering and locale-aware sorting stay in the service // layer where Cyrillic ordinal semantics are preserved. var q = _db.Events.AsNoTracking() .Where(e => e.ScheduledAt.CompareTo(fromStr) >= 0 && e.ScheduledAt.CompareTo(toStr) <= 0); if (query.SportCodes is { Count: > 0 } sports) { var sportArray = sports.Distinct().ToArray(); q = q.Where(e => sportArray.Contains(e.SportCode)); } var entities = await q.ToListAsync(ct); return entities.Select(Mapping.ToDomain).ToList().AsReadOnly(); } public async Task> GetManyAsync( IReadOnlyCollection ids, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(ids); var result = new Dictionary(ids.Count); if (ids.Count == 0) return result; var codes = ids.Select(e => e.Value).Distinct().ToArray(); var entities = await _db.Events.AsNoTracking() .Where(e => codes.Contains(e.EventCode)) .ToListAsync(ct); foreach (var entity in entities) { var domain = Mapping.ToDomain(entity); result[domain.Id] = domain; } return result; } public async Task> ListBySportAsync(SportCode sport, CancellationToken ct = default) { var entities = await _db.Events.AsNoTracking() .Where(e => e.SportCode == sport.Value) .ToListAsync(ct); return entities.Select(Mapping.ToDomain).ToList().AsReadOnly(); } public Task CountAsync(CancellationToken ct = default) => _db.Events.AsNoTracking().CountAsync(ct); public async Task> ListDistinctSportCodesAsync(CancellationToken ct = default) { var codes = await _db.Events.AsNoTracking() .Select(e => e.SportCode) .Distinct() .ToListAsync(ct); codes.Sort(); return codes; } public async Task> ListDistinctCountryCodesAsync(CancellationToken ct = default) { var codes = await _db.Events.AsNoTracking() .Select(e => e.CountryCode) .Distinct() .ToListAsync(ct); codes.Sort(StringComparer.OrdinalIgnoreCase); return codes; } public async Task AddAsync(Event entity, CancellationToken ct = default) { var efEntity = Mapping.ToEntity(entity); await _db.Events.AddAsync(efEntity, ct); } public Task UpdateAsync(Event entity, CancellationToken ct = default) { var efEntity = Mapping.ToEntity(entity); _db.Events.Update(efEntity); return Task.CompletedTask; } public async Task DeleteAsync(EventId key, CancellationToken ct = default) { var entity = await _db.Events.FindAsync([key.Value], ct); if (entity is not null) _db.Events.Remove(entity); } public async Task SaveChangesAsync(CancellationToken ct = default) => await _db.SaveChangesAsync(ct); }