using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Diagnostics.Metrics; using System.Security.Cryptography; using System.Text; using System.Text.Json; using Tiku.Application.Learning; using Tiku.Application.QuestionBanks; using Tiku.Application.Security; using Tiku.Domain.Common; using Tiku.Domain.Content; using Tiku.Domain.Learning; using Tiku.Domain.QuestionBanks; using Tiku.Infrastructure.Persistence; using ZLinq; namespace Tiku.Infrastructure.Learning; public sealed partial class LearningActivityService { public async Task> GetFavoriteQuestionsAsync( LearningActor actor, LearningLimitFilter filter, CancellationToken cancellationToken = default) { var items = await dbContext.FavoriteQuestions .AsNoTracking() .Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId) .OrderByDescending(item => item.CreatedAt) .Take(ResolveLimit(filter.Limit)) .Select(item => new FavoriteQuestionItem( item.QuestionReferenceId, new QuestionLocator( item.QuestionOwnerTenantId == item.TenantId ? QuestionSource.Tenant : QuestionSource.Platform, item.QuestionId), item.CreatedAt)) .ToArrayAsync(cancellationToken); return new LearningList(items); } public async Task ToggleFavoriteQuestionAsync( LearningActor actor, QuestionActionCommand command, CancellationToken cancellationToken = default) { var reference = await questionReferenceService.ResolveAsync( actor.TenantId, actor.UserId, command.Locator, cancellationToken); var favorite = command.Favorite ?? true; var item = await dbContext.FavoriteQuestions.FindAsync( [actor.TenantId, actor.UserId, reference.Id], cancellationToken); if (favorite) { if (item is null) { dbContext.FavoriteQuestions.Add(new FavoriteQuestion { TenantId = actor.TenantId, UserId = actor.UserId, QuestionReferenceId = reference.Id, QuestionOwnerTenantId = reference.QuestionOwnerTenantId, QuestionId = reference.QuestionId, Source = reference.Source.ToString().ToLowerInvariant(), CreatedAt = DateTimeOffset.UtcNow }); } } else if (item is not null) { dbContext.FavoriteQuestions.Remove(item); } await dbContext.SaveChangesAsync(cancellationToken); return new LearningActionResult(true, favorite); } public async Task> GetWrongQuestionsAsync( LearningActor actor, LearningLimitFilter filter, CancellationToken cancellationToken = default) { var query = dbContext.WrongQuestions .AsNoTracking() .Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId); if (!string.Equals(filter.Status, "all", StringComparison.OrdinalIgnoreCase)) { query = query.Where(item => item.ResolvedAt == null); } var items = await query .OrderByDescending(item => item.LastWrongAt) .Take(ResolveLimit(filter.Limit)) .Select(item => new WrongQuestionItem( item.QuestionReferenceId, new QuestionLocator( item.QuestionOwnerTenantId == item.TenantId ? QuestionSource.Tenant : QuestionSource.Platform, item.QuestionId), item.WrongCount, item.LastWrongAt, item.ResolvedAt)) .ToArrayAsync(cancellationToken); return new LearningList(items); } public async Task ResolveWrongQuestionAsync( LearningActor actor, QuestionActionCommand command, CancellationToken cancellationToken = default) { var reference = await questionReferenceService.ResolveAsync( actor.TenantId, actor.UserId, command.Locator, cancellationToken); var item = await dbContext.WrongQuestions.FindAsync( [actor.TenantId, actor.UserId, reference.Id], cancellationToken); if (item is null) { throw new LearningResourceNotFoundException("wrong_question_not_found", "Wrong question was not found."); } item.ResolvedAt = DateTimeOffset.UtcNow; await dbContext.SaveChangesAsync(cancellationToken); return new LearningActionResult(true); } public async Task GetWrongQuestionReviewPlanAsync( LearningActor actor, LearningLimitFilter filter, CancellationToken cancellationToken = default) { var items = await dbContext.WrongQuestions.AsNoTracking() .Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId && item.ResolvedAt == null) .OrderByDescending(item => item.WrongCount) .ThenBy(item => item.LastWrongAt) .Take(ResolveLimit(filter.Limit)) .Select(item => new WrongQuestionReviewPlanItem( item.QuestionReferenceId, new QuestionLocator( item.QuestionOwnerTenantId == item.TenantId ? QuestionSource.Tenant : QuestionSource.Platform, item.QuestionId), item.WrongCount, item.LastWrongAt)) .ToArrayAsync(cancellationToken); return new WrongQuestionReviewPlan( items, JsonSerializer.SerializeToElement(new { mode = "wrong_review", questionCount = items.Length, recommendedEndpoint = "/api/student/learning/practice-sessions" })); } }