using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using NexusReader.Application.Abstractions.Messaging; using NexusReader.Application.DTOs.Creator; using NexusReader.Data.Persistence; namespace NexusReader.Application.Queries.Creator; /// /// Query to load aggregated Creator Dashboard telemetry metrics and book listings. /// /// The ID of the creator requesting dashboard data. /// The tenant ID for multi-tenant isolation. public record GetCreatorDashboardDataQuery(string UserId, string TenantId) : IQuery; /// /// Handler that executes projection-only LINQ queries to aggregate metrics and compute word counts /// without loading raw chapter content into memory or tracking them in the EF Core Change Tracker. /// public class GetCreatorDashboardDataQueryHandler : IQueryHandler { private readonly IDbContextFactory _dbContextFactory; public GetCreatorDashboardDataQueryHandler(IDbContextFactory dbContextFactory) { _dbContextFactory = dbContextFactory; } public async Task> Handle(GetCreatorDashboardDataQuery request, CancellationToken cancellationToken) { using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); // Execute projection-only LINQ query. The heavy MarkdownContent is projected only as integer lengths. var projectedBooks = await dbContext.Books .AsNoTracking() .Where(b => b.UserId == request.UserId && b.TenantId == request.TenantId) .Select(b => new { b.Id, b.Title, LivePublishedRevision = b.LivePublishedRevision == null ? null : new CreatorBookRevisionDto( b.LivePublishedRevision.Id, b.LivePublishedRevision.VersionString, b.LivePublishedRevision.IsPublished, b.LivePublishedRevision.CreatedAt, b.LivePublishedRevision.PublishedAt ), CurrentDraftRevision = b.CurrentDraftRevision == null ? null : new CreatorBookRevisionDto( b.CurrentDraftRevision.Id, b.CurrentDraftRevision.VersionString, b.CurrentDraftRevision.IsPublished, b.CurrentDraftRevision.CreatedAt, b.CurrentDraftRevision.PublishedAt ), FirstChapterId = b.CurrentDraftRevision == null ? (Guid?)null : b.CurrentDraftRevision.Chapters.OrderBy(c => c.SortOrder).Select(c => c.Id).FirstOrDefault(), ChapterContentLengths = b.CurrentDraftRevision == null ? new List() : b.CurrentDraftRevision.Chapters.Select(c => c.MarkdownContent.Length).ToList() }) .ToListAsync(cancellationToken); var booksList = new List(); int totalReads = 0; int totalWords = 0; foreach (var pBook in projectedBooks) { // Estimate word count (approx. 6 characters per word as a database-friendly standard length) int wordCount = pBook.ChapterContentLengths.Sum(len => len / 6); totalWords += wordCount; // Generate deterministic simulated telemetry metrics scoped to this Book int bookReads = Math.Abs(pBook.Id.GetHashCode() % 1000) + 120; totalReads += bookReads; var bookDto = new CreatorBookDto( pBook.Id, pBook.Title, wordCount, bookReads, pBook.FirstChapterId, pBook.LivePublishedRevision, pBook.CurrentDraftRevision ); booksList.Add(bookDto); } // Calculate aggregate dashboard metrics based on projected stats int activeReaders = projectedBooks.Count == 0 ? 0 : Math.Abs(request.UserId.GetHashCode() % 15) + 3; decimal grossRevenue = totalReads * 1.49m; double avgReadTime = projectedBooks.Count == 0 ? 0 : Math.Round(totalWords / 250.0, 1); // standard 250 words per minute reading speed var metrics = new DashboardMetricsDto( totalReads, avgReadTime, activeReaders, grossRevenue ); return FluentResults.Result.Ok(new CreatorDashboardDataDto(metrics, booksList)); } }