using PlotLine.Models; namespace PlotLine.Services; public sealed class CharacterAgeContinuityScene { public int SceneID { get; init; } public int ProjectID { get; init; } public int? BookID { get; init; } public int? ChapterID { get; init; } public decimal? ChapterNumber { get; init; } public string? ChapterTitle { get; init; } public decimal? SceneNumber { get; init; } public string SceneTitle { get; init; } = string.Empty; public int ChapterSortOrder { get; init; } public int SceneSortOrder { get; init; } public string TimeModeName { get; init; } = string.Empty; public DateTime? StartDateTime { get; init; } } public static class CharacterAgeContinuityService { public static IReadOnlyList GetWarningsForScene( CharacterAgeContinuityScene currentScene, IReadOnlyList scenes, IReadOnlyList appearances, IReadOnlyList currentSceneCharacters) { var warnings = new List(); warnings.AddRange(GetApproximateAgeMismatchWarnings(currentScene, currentSceneCharacters)); warnings.AddRange(GetBirthdayTransitionWarnings(currentScene, scenes, appearances)); return warnings; } public static IReadOnlyList GetApproximateAgeMismatchWarnings( CharacterAgeContinuityScene scene, IReadOnlyList sceneCharacters) { var sceneDate = CharacterAgeDisplayService.ResolveEffectiveSceneDate(scene.TimeModeName, scene.StartDateTime); if (!sceneDate.HasValue) { return []; } return sceneCharacters .Where(character => character.BirthDate.HasValue && character.AgeAtSeriesStart.HasValue) .Select(character => new { Character = character, ExactAge = CharacterAgeDisplayService.CalculateExactAge(character.BirthDate!.Value.Date, sceneDate.Value.Date) }) .Where(item => Math.Abs(item.Character.AgeAtSeriesStart!.Value - item.ExactAge.Years) > 1) .Select(item => CreateWarning( scene, item.Character.CharacterID, "Warning", "Character Age", "Character profile age differs significantly from calculated age.", $"Profile states approximately {item.Character.AgeAtSeriesStart} years old. Calculated age in this scene is {CharacterAgeDisplayService.FormatExactAge(item.Character.BirthDate!.Value.Date, sceneDate.Value.Date)}.")) .ToList(); } public static IReadOnlyList GetBirthdayTransitionWarnings( CharacterAgeContinuityScene currentScene, IReadOnlyList scenes, IReadOnlyList appearances) { var orderedScenes = scenes .Select(scene => new { Scene = scene, EffectiveDate = CharacterAgeDisplayService.ResolveEffectiveSceneDate(scene.TimeModeName, scene.StartDateTime) }) .Where(scene => scene.EffectiveDate.HasValue) .OrderBy(scene => scene.EffectiveDate!.Value) .ThenBy(scene => scene.Scene.ChapterSortOrder) .ThenBy(scene => scene.Scene.SceneSortOrder) .ThenBy(scene => scene.Scene.SceneID) .ToList(); var currentIndex = orderedScenes.FindIndex(scene => scene.Scene.SceneID == currentScene.SceneID); if (currentIndex < 0 || currentIndex == orderedScenes.Count - 1) { return []; } var current = orderedScenes[currentIndex]; var next = orderedScenes[currentIndex + 1]; if (next.EffectiveDate!.Value <= current.EffectiveDate!.Value) { return []; } var nextSceneCharacterIds = appearances .Where(appearance => appearance.SceneID == next.Scene.SceneID) .Select(appearance => appearance.CharacterID) .ToHashSet(); return appearances .Where(appearance => appearance.SceneID == current.Scene.SceneID && appearance.BirthDate.HasValue && nextSceneCharacterIds.Contains(appearance.CharacterID)) .GroupBy(appearance => appearance.CharacterID) .Select(group => group.First()) .Select(appearance => new { Appearance = appearance, Birthday = GetNextBirthdayAfter(appearance.BirthDate!.Value.Date, current.EffectiveDate!.Value.Date) }) .Where(item => item.Birthday <= next.EffectiveDate!.Value.Date) .Select(item => { var age = CharacterAgeDisplayService.CalculateExactAge(item.Appearance.BirthDate!.Value.Date, item.Birthday).Years; return CreateWarning( current.Scene, item.Appearance.CharacterID, "Info", "Character Age", $"{item.Appearance.CharacterName} turns {age} between this scene and the next.", $"Birthday falls on {item.Birthday:dd MMM yyyy}. Next scene: {next.Scene.SceneTitle}."); }) .ToList(); } private static DateTime GetNextBirthdayAfter(DateTime birthDate, DateTime sceneDate) { var birthday = new DateTime(sceneDate.Year, birthDate.Month, birthDate.Day); return birthday > sceneDate ? birthday : birthday.AddYears(1); } private static ContinuityWarning CreateWarning( CharacterAgeContinuityScene scene, int? characterId, string severityName, string warningTypeName, string message, string details) => new() { ContinuityWarningID = 0, ProjectID = scene.ProjectID, BookID = scene.BookID, ChapterID = scene.ChapterID, ChapterNumber = scene.ChapterNumber, ChapterTitle = scene.ChapterTitle, SceneID = scene.SceneID, SceneNumber = scene.SceneNumber, SceneTitle = scene.SceneTitle, EntityType = "Character", EntityID = characterId, WarningTypeName = warningTypeName, SeverityName = severityName, Message = message, Details = details, CreatedDate = DateTime.UtcNow, LastDetectedDate = DateTime.UtcNow }; }