using System.Text; using System.Text.RegularExpressions; using FluentResults; using NexusReader.Application.Abstractions.Services; using NexusReader.Application.Queries.Reader; using VersOne.Epub; namespace NexusReader.Infrastructure.Services; public class EpubService : IEpubService { private const string EpubPath = "wwwroot/assets/book.epub"; private const int WordThreshold = 1000; public async Task> GetEpubContentAsync(int chapterIndex) { try { // Path handling: Recursive search upwards to find the asset in development or production var relativePath = Path.Combine("wwwroot", "assets", "book.epub"); string? fullPath = null; var searchPaths = new List(); var currentDir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); while (currentDir != null) { var checkPath1 = Path.Combine(currentDir.FullName, relativePath); var checkPath2 = Path.Combine(currentDir.FullName, "src", "NexusReader.Web.New", relativePath); searchPaths.Add(checkPath1); if (File.Exists(checkPath1)) { fullPath = checkPath1; break; } searchPaths.Add(checkPath2); if (File.Exists(checkPath2)) { fullPath = checkPath2; break; } currentDir = currentDir.Parent; } if (fullPath == null) { return Result.Fail($"EPUB file not found. Checked {searchPaths.Count} locations, including: {string.Join(", ", searchPaths.Take(3))}"); } EpubBook book = await EpubReader.ReadBookAsync(fullPath); var blocks = new List(); int totalWordCount = 0; int blockCounter = 0; if (book.ReadingOrder == null || !book.ReadingOrder.Any()) { return Result.Fail("The EPUB has no readable content files in ReadingOrder."); } // Ensure index is within bounds if (chapterIndex < 0 || chapterIndex >= book.ReadingOrder.Count) { chapterIndex = 0; // Default to first chapter } var chapter = book.ReadingOrder[chapterIndex]; var chapterTitle = chapter.FilePath ?? $"Chapter {chapterIndex + 1}"; var paragraphs = ExtractParagraphs(chapter.Content); foreach (var p in paragraphs) { var sanitizedContent = SanitizeParagraph(p); if (string.IsNullOrWhiteSpace(sanitizedContent)) continue; // Requirement: Each paragraph mapped to its own TextSegmentBlock blocks.Add(new TextSegmentBlock($"seg-{blockCounter++}", sanitizedContent)); int wordsInP = CountWords(sanitizedContent); totalWordCount += wordsInP; // Requirement: Smart Injection after 1000 words if (totalWordCount >= WordThreshold) { blocks.Add(CreateAiTrigger($"trigger-{blockCounter++}")); totalWordCount = 0; } } // End of chapter section trigger if (blocks.Any() && blocks.Last() is not AiActionTriggerBlock) { blocks.Add(CreateAiTrigger($"trigger-{blockCounter++}")); } return Result.Ok(new ReaderPageViewModel(blocks, chapterIndex, book.ReadingOrder.Count, chapterTitle)); } catch (Exception ex) { return Result.Fail(new Error($"Failed to process EPUB: {ex.Message}").CausedBy(ex)); } } private List ExtractParagraphs(string html) { var paragraphs = new List(); // Match

tags and their content var matches = Regex.Matches(html, @"]*>(.*?)

", RegexOptions.IgnoreCase | RegexOptions.Singleline); foreach (Match match in matches) { paragraphs.Add(match.Groups[1].Value); } // Fallback: split by double newlines if no

tags found if (paragraphs.Count == 0) { var bodyMatch = Regex.Match(html, @"]*>(.*?)", RegexOptions.IgnoreCase | RegexOptions.Singleline); var content = bodyMatch.Success ? bodyMatch.Groups[1].Value : html; paragraphs = content.Split(new[] { "
", "
", "\n\n" }, StringSplitOptions.RemoveEmptyEntries).ToList(); } return paragraphs; } private string SanitizeParagraph(string html) { // 1. Remove