using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using GermanApp.Application.DTOs; using GermanApp.Domain.Entities; using GermanApp.Domain.Interfaces; using Microsoft.Extensions.Logging; namespace GermanApp.Application.Services; /// /// Application service for managing story segments. /// This is part of the Application layer. /// public class StoryService { private readonly IStoryRepository _storyRepository; private readonly IStoryProgressRepository _progressRepository; private readonly ILogger _logger; /// /// Creates a new StoryService. /// /// Repository for story segments /// Repository for story progress /// Logger for service operations public StoryService( IStoryRepository storyRepository, IStoryProgressRepository progressRepository, ILogger logger) { _storyRepository = storyRepository; _progressRepository = progressRepository; _logger = logger; } /// /// Gets a story segment by its ID. /// /// The segment ID /// Cancellation token /// The story segment DTO, or null if not found public virtual async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) { _logger.LogInformation("Getting story segment by ID: {Id}", id); var segment = await _storyRepository.GetByIdAsync(id, cancellationToken); if (segment == null) { _logger.LogWarning("Story segment not found: {Id}", id); return null; } return StorySegmentDto.FromEntity(segment); } /// /// Gets all story segments for a specific level. /// /// The level ID /// Whether to include inactive segments /// Cancellation token /// List of story segment DTOs public virtual async Task> GetByLevelAsync( int levelId, bool includeInactive = false, CancellationToken cancellationToken = default) { _logger.LogInformation("Getting story segments for level: {LevelId}", levelId); var segments = await _storyRepository.GetByLevelAsync(levelId, includeInactive, cancellationToken); return segments.Select(StorySegmentDto.FromEntity).ToList(); } /// /// Gets all story segments for a specific lesson. /// /// The lesson ID /// Cancellation token /// List of story segment DTOs public virtual async Task> GetByLessonAsync( int lessonId, CancellationToken cancellationToken = default) { _logger.LogInformation("Getting story segments for lesson: {LessonId}", lessonId); var segments = await _storyRepository.GetByLessonAsync(lessonId, cancellationToken); return segments.Select(StorySegmentDto.FromEntity).ToList(); } /// /// Creates a new story segment. /// /// The DTO containing segment data /// Cancellation token /// The created story segment DTO public virtual async Task CreateAsync( CreateStorySegmentDto dto, CancellationToken cancellationToken = default) { _logger.LogInformation( "Creating story segment: LevelId={LevelId}, Order={Order}, Title={Title}", dto.LevelId, dto.Order, dto.Title); // Check if a segment with the same level and order already exists var existing = await _storyRepository.GetByOrderRangeAsync( dto.LevelId, dto.Order, dto.Order, cancellationToken); if (existing.Any()) { _logger.LogError( "Story segment with LevelId={LevelId} and Order={Order} already exists", dto.LevelId, dto.Order); throw new InvalidOperationException( $"A story segment with Order={dto.Order} already exists for level {dto.LevelId}"); } // Create the entity var segment = StorySegment.Create( dto.LevelId, dto.LessonId, dto.Content, dto.Order, dto.Title, dto.Theme, dto.EstimatedReadingMinutes); // Save to repository var created = await _storyRepository.AddAsync(segment, cancellationToken); _logger.LogInformation("Created story segment with ID: {Id}", created.Id); return StorySegmentDto.FromEntity(created); } /// /// Updates an existing story segment. /// /// The segment ID /// The DTO containing updated data /// Cancellation token /// The updated story segment DTO, or null if not found public virtual async Task UpdateAsync( int id, UpdateStorySegmentDto dto, CancellationToken cancellationToken = default) { _logger.LogInformation("Updating story segment: {Id}", id); var segment = await _storyRepository.GetByIdAsync(id, cancellationToken); if (segment == null) { _logger.LogWarning("Story segment not found for update: {Id}", id); return null; } // Apply updates if (dto.Content != null) segment.UpdateContent(dto.Content); if (dto.Title != null) segment.UpdateTitle(dto.Title); if (dto.Theme != null) segment.UpdateTheme(dto.Theme); if (dto.Order != null) segment.UpdateOrder(dto.Order.Value); if (dto.EstimatedReadingMinutes != null) segment.UpdateEstimatedReadingMinutes(dto.EstimatedReadingMinutes.Value); if (dto.LessonId != null) segment.UpdateLesson(dto.LessonId); if (dto.IsActive != null) { if (dto.IsActive.Value) segment.Activate(); else segment.Deactivate(); } // Save changes await _storyRepository.UpdateAsync(segment, cancellationToken); _logger.LogInformation("Updated story segment: {Id}", id); return StorySegmentDto.FromEntity(segment); } /// /// Deletes a story segment by its ID. /// /// The segment ID /// Cancellation token /// True if the segment was deleted, false if not found public virtual async Task DeleteAsync(int id, CancellationToken cancellationToken = default) { _logger.LogInformation("Deleting story segment: {Id}", id); var exists = await _storyRepository.ExistsAsync(id, cancellationToken); if (!exists) { _logger.LogWarning("Story segment not found for deletion: {Id}", id); return false; } await _storyRepository.DeleteAsync(id, cancellationToken); _logger.LogInformation("Deleted story segment: {Id}", id); return true; } /// /// Gets the next segment to unlock for a user after completing a lesson. /// /// The level ID /// The order of the completed lesson /// Cancellation token /// The next story segment DTO, or null if none public virtual async Task GetNextSegmentToUnlockAsync( int levelId, int completedLessonOrder, CancellationToken cancellationToken = default) { _logger.LogInformation( "Getting next segment to unlock for level {LevelId} after lesson {LessonOrder}", levelId, completedLessonOrder); var segment = await _storyRepository.GetNextSegmentToUnlockAsync( levelId, completedLessonOrder, cancellationToken); if (segment == null) { _logger.LogInformation("No segment to unlock for level {LevelId} after lesson {LessonOrder}", levelId, completedLessonOrder); return null; } return StorySegmentDto.FromEntity(segment); } /// /// Checks if a user has unlocked a specific story segment. /// /// The user ID /// The segment ID /// Cancellation token /// True if the segment is unlocked public virtual async Task IsSegmentUnlockedAsync( int userId, int segmentId, CancellationToken cancellationToken = default) { return await _progressRepository.IsSegmentUnlockedAsync(userId, segmentId, cancellationToken); } /// /// Checks if a user has completed (read/listened to) a specific story segment. /// /// The user ID /// The segment ID /// Cancellation token /// True if the segment is completed public virtual async Task IsSegmentCompletedAsync( int userId, int segmentId, CancellationToken cancellationToken = default) { return await _progressRepository.IsSegmentCompletedAsync(userId, segmentId, cancellationToken); } /// /// Gets all segments that need audio generation. /// /// Cancellation token /// List of story segment DTOs needing audio public virtual async Task> GetSegmentsNeedingAudioAsync( CancellationToken cancellationToken = default) { _logger.LogInformation("Getting story segments needing audio generation"); var segments = await _storyRepository.GetSegmentsNeedingAudioAsync(cancellationToken); return segments.Select(StorySegmentDto.FromEntity).ToList(); } /// /// Updates the audio URL for a story segment. /// /// The segment ID /// The audio URL /// Cancellation token /// The updated story segment DTO, or null if not found public virtual async Task UpdateAudioUrlAsync( int id, string audioUrl, CancellationToken cancellationToken = default) { _logger.LogInformation("Updating audio URL for story segment: {Id}", id); var segment = await _storyRepository.GetByIdAsync(id, cancellationToken); if (segment == null) { _logger.LogWarning("Story segment not found for audio update: {Id}", id); return null; } segment.UpdateAudioUrl(audioUrl); await _storyRepository.UpdateAsync(segment, cancellationToken); _logger.LogInformation("Updated audio URL for story segment: {Id}", id); return StorySegmentDto.FromEntity(segment); } /// /// Gets a user's progress through a story. /// /// The user ID /// The level ID /// Cancellation token /// Story progress DTO public virtual async Task GetUserProgressAsync( int userId, int levelId, CancellationToken cancellationToken = default) { _logger.LogInformation("Getting story progress for user {UserId} in level {LevelId}", userId, levelId); // Get all segments for the level var segments = await _storyRepository.GetByLevelAsync(levelId, false, cancellationToken); if (!segments.Any()) { _logger.LogWarning("No story segments found for level: {LevelId}", levelId); return new StoryProgressDto( levelId, "Unknown", 0, 0, 0, Array.Empty()); } // Get user's progress for this level var userProgress = await _progressRepository.GetByUserAndLevelAsync(userId, levelId, cancellationToken); var totalSegments = segments.Count; var unlockedSegments = userProgress.Count; var highestUnlocked = await _progressRepository.GetHighestUnlockedOrderAsync( userId, levelId, cancellationToken); // Build segment progress list var segmentProgress = new List(); foreach (var segment in segments) { var isUnlocked = await _progressRepository.IsSegmentUnlockedAsync( userId, segment.Id, cancellationToken); var isCompleted = await _progressRepository.IsSegmentCompletedAsync( userId, segment.Id, cancellationToken); segmentProgress.Add(new StorySegmentProgressDto( segment.Id, segment.Order, segment.Title, isUnlocked, isCompleted)); } // Get level name from first segment (or use code as fallback) var levelName = segments.First().Level?.Name ?? segments.First().Level?.Code ?? "Unknown"; return new StoryProgressDto( levelId, levelName, totalSegments, unlockedSegments, highestUnlocked, segmentProgress); } /// /// Unlocks the next story segment for a user after completing a lesson. /// /// The user ID /// The level ID /// The order of the completed lesson /// Cancellation token /// The unlocked story segment DTO, or null if none to unlock public virtual async Task UnlockNextSegmentAsync( int userId, int levelId, int completedLessonOrder, CancellationToken cancellationToken = default) { _logger.LogInformation( "Unlocking next story segment for user {UserId} in level {LevelId} after lesson {LessonOrder}", userId, levelId, completedLessonOrder); // Get the next segment to unlock var segment = await _storyRepository.GetNextSegmentToUnlockAsync( levelId, completedLessonOrder, cancellationToken); if (segment == null) { _logger.LogInformation( "No segment to unlock for user {UserId} in level {LevelId} after lesson {LessonOrder}", userId, levelId, completedLessonOrder); return null; } // Check if user already has this segment unlocked var alreadyUnlocked = await _progressRepository.IsSegmentUnlockedAsync( userId, segment.Id, cancellationToken); if (alreadyUnlocked) { _logger.LogInformation( "Segment {SegmentId} already unlocked for user {UserId}", segment.Id, userId); return StorySegmentDto.FromEntity(segment); } // Create progress record var progress = StoryProgress.Create(userId, levelId, segment.Id); await _progressRepository.AddAsync(progress, cancellationToken); _logger.LogInformation( "Unlocked segment {SegmentId} for user {UserId} in level {LevelId}", segment.Id, userId, levelId); return StorySegmentDto.FromEntity(segment); } /// /// Marks a story segment as completed (read/listened to) for a user. /// /// The user ID /// The segment ID /// Cancellation token /// True if the segment was marked as completed, false if not found or already completed public virtual async Task MarkSegmentAsCompletedAsync( int userId, int segmentId, CancellationToken cancellationToken = default) { _logger.LogInformation("Marking segment {SegmentId} as completed for user {UserId}", segmentId, userId); var progress = await _progressRepository.GetByUserAndSegmentAsync( userId, segmentId, cancellationToken); if (progress == null) { _logger.LogWarning( "Progress record not found for user {UserId} and segment {SegmentId}", userId, segmentId); return false; } if (progress.IsCompleted) { _logger.LogInformation("Segment already completed for user {UserId}", userId); return false; } progress.MarkAsCompleted(); await _progressRepository.UpdateAsync(progress, cancellationToken); _logger.LogInformation("Marked segment {SegmentId} as completed for user {UserId}", segmentId, userId); return true; } }