using MongoDB.Bson; using MongoDB.Driver; using QuestionsApi.Models; namespace QuestionsApi.Services; public class QuestionReportStore { private readonly IMongoCollection _reports; public QuestionReportStore(MongoDatabaseProvider mongo) { _reports = mongo.Database.GetCollection("QuestionReports"); _reports.Indexes.CreateMany( [ new CreateIndexModel( Builders.IndexKeys .Ascending(r => r.Status) .Descending(r => r.CreatedUtc)), new CreateIndexModel( Builders.IndexKeys.Ascending(r => r.QuestionExternalId)), new CreateIndexModel( Builders.IndexKeys.Ascending(r => r.UserId)) ]); } public async Task CreateAsync(Question question, string userId, string category, string details) { var now = DateTime.UtcNow; var report = new QuestionReport { QuestionExternalId = question.ExternalId, QuestionMongoId = question.Id ?? "", UserId = userId, Category = category, Details = details, Status = QuestionReportStatus.Open, QuestionSnapshot = new QuestionReportSnapshot { Stem = question.Stem, LeadIn = question.LeadIn, Choices = question.Choices .Select(choice => new QuestionChoice { Id = choice.Id, Text = choice.Text }) .ToList(), CorrectChoiceId = question.CorrectChoiceId, Explanation = question.Explanation, KeyTakeaway = question.KeyTakeaway, UsmlePearl = question.UsmlePearl, ReviewFollowups = question.ReviewFollowups, ChoiceExplanations = new Dictionary(question.ChoiceExplanations), WrongAnswerFollowups = question.WrongAnswerFollowups.ToDictionary( item => item.Key, item => new WrongAnswerFollowup { Prompt = item.Value.Prompt, Choices = item.Value.Choices .Select(choice => new QuestionChoice { Id = choice.Id, Text = choice.Text }) .ToList(), CorrectChoiceId = item.Value.CorrectChoiceId, TeachingPoint = item.Value.TeachingPoint }), Labs = question.Labs .Select(lab => new QuestionLab { Name = lab.Name, Value = lab.Value, Unit = lab.Unit, Reference = lab.Reference }) .ToList(), Media = [.. question.Media] }, CreatedUtc = now, UpdatedUtc = now }; await _reports.InsertOneAsync(report); return report; } public async Task GetByIdAsync(string id) { if (!ObjectId.TryParse(id, out _)) return null; return await _reports.Find(r => r.Id == id).FirstOrDefaultAsync(); } public async Task SearchAsync( string status, string category, string questionExternalId, string userId, int skip, int limit) { var filter = Builders.Filter.Empty; if (!string.Equals(status, "all", StringComparison.OrdinalIgnoreCase)) filter &= Builders.Filter.Eq(r => r.Status, status); if (!string.IsNullOrWhiteSpace(category)) filter &= Builders.Filter.Eq(r => r.Category, category); if (!string.IsNullOrWhiteSpace(questionExternalId)) filter &= Builders.Filter.Eq(r => r.QuestionExternalId, questionExternalId); if (!string.IsNullOrWhiteSpace(userId)) filter &= Builders.Filter.Eq(r => r.UserId, userId); var total = await _reports.CountDocumentsAsync(filter); var items = await _reports .Find(filter) .SortByDescending(r => r.CreatedUtc) .Skip(skip) .Limit(limit) .ToListAsync(); return new QuestionReportListResponse { Items = items, Total = total, Skip = skip, Limit = limit }; } public async Task CloseAsync(string id, string status, string reviewerUserId, string reviewerNote) { if (!ObjectId.TryParse(id, out _)) return null; var now = DateTime.UtcNow; var filter = Builders.Filter.And( Builders.Filter.Eq(r => r.Id, id), Builders.Filter.Eq(r => r.Status, QuestionReportStatus.Open)); var update = Builders.Update .Set(r => r.Status, status) .Set(r => r.ReviewerNote, reviewerNote) .Set(r => r.ReviewedByUserId, reviewerUserId) .Set(r => r.ReviewedUtc, now) .Set(r => r.UpdatedUtc, now); return await _reports.FindOneAndUpdateAsync( filter, update, new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); } }