using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using FluentResults; using Microsoft.EntityFrameworkCore; using NexusReader.Application.Abstractions.Messaging; using NexusReader.Application.Queries.Graph; using NexusReader.Data.Persistence; namespace NexusReader.Application.Queries.Concepts; internal sealed class GetBookConceptsMapQueryHandler : IQueryHandler { private readonly IDbContextFactory _dbContextFactory; public GetBookConceptsMapQueryHandler(IDbContextFactory dbContextFactory) { _dbContextFactory = dbContextFactory; } public async Task> Handle(GetBookConceptsMapQuery request, CancellationToken cancellationToken) { using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); // 1. Fetch user to extract reading progress (LastReadPageId) var user = await dbContext.Users .Where(u => u.Id == request.UserId) .Select(u => new { u.LastReadPageId }) .FirstOrDefaultAsync(cancellationToken); if (user == null) { return Result.Fail("User not found."); } var lastReadBlockId = user.LastReadPageId ?? string.Empty; // 2. Fetch all KnowledgeUnits associated with the ebook and user's tenant var units = await dbContext.KnowledgeUnits .Where(k => k.EbookId == request.BookId && (k.TenantId == request.TenantId || k.TenantId == "global" || string.IsNullOrEmpty(k.TenantId))) .OrderBy(k => k.CreatedAt) .ToListAsync(cancellationToken); var nodes = new List(); foreach (var unit in units) { // Only process units representing sections or conceptual milestones (usually starting with "seg-") if (string.IsNullOrEmpty(unit.Id) || !unit.Id.StartsWith("seg-")) { continue; } string label = unit.Id; string group = "concept"; string summary = unit.Content; var keyTerms = new List(); if (!string.IsNullOrEmpty(unit.MetadataJson)) { try { using var doc = JsonDocument.Parse(unit.MetadataJson); if (doc.RootElement.TryGetProperty("label", out var labelProp)) label = labelProp.GetString() ?? label; if (doc.RootElement.TryGetProperty("group", out var groupProp)) group = groupProp.GetString() ?? group; if (doc.RootElement.TryGetProperty("summary", out var summaryProp)) summary = summaryProp.GetString() ?? summary; if (doc.RootElement.TryGetProperty("key_terms", out var ktProp) && ktProp.ValueKind == JsonValueKind.Array) { foreach (var term in ktProp.EnumerateArray()) { if (term.GetString() is string s) keyTerms.Add(s); } } } catch { // Fallback to defaults } } nodes.Add(new GraphNodeDto( Id: unit.Id, Label: label, Group: group, Description: unit.Content, Type: unit.Type.ToString(), Summary: summary, KeyTerms: keyTerms )); } // Return sorted by the numeric value in the seg-ID to ensure topdown vertical alignment var sortedNodes = nodes .OrderBy(n => ParseSegmentNumber(n.Id)) .ToList(); return Result.Ok(new BookConceptsMapResultDto(sortedNodes, lastReadBlockId)); } private static int ParseSegmentNumber(string? id) { if (string.IsNullOrEmpty(id)) return 0; var digits = new string(id.Where(char.IsDigit).ToArray()); return int.TryParse(digits, out var val) ? val : 0; } }