using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using PlotLine.Data; using PlotLine.Models; using PlotLine.Services; using PlotLine.ViewModels; namespace PlotLine.Controllers; [Authorize(Policy = "AdminOnly")] [Route("Development")] public sealed class DevelopmentController( IWebHostEnvironment environment, ICurrentUserService currentUser, IIllustrationLibraryService illustrationLibrary, IStoryMemoryService storyMemory, IStoryMemoryRepository storyMemoryRepository, IStoryIntelligenceVisualisationSnapshotService snapshots) : Controller { [HttpGet("StoryIntelligenceExperience")] public async Task StoryIntelligenceExperience([FromQuery] string? mode, [FromQuery] int? importSessionId, [FromQuery] int? runId) { if (!environment.IsDevelopment()) { return NotFound(); } var requestedMode = string.IsNullOrWhiteSpace(mode) ? "real" : mode.Trim(); var simulationRequested = string.Equals(requestedMode, "simulation", StringComparison.OrdinalIgnoreCase); var replayRequested = string.Equals(requestedMode, "replay", StringComparison.OrdinalIgnoreCase); if (!simulationRequested && importSessionId.HasValue && currentUser.UserId is int userId) { var realModel = replayRequested ? await snapshots.BuildReplaySnapshotAsync(importSessionId.Value, userId) : await snapshots.BuildImportSessionSnapshotAsync(importSessionId.Value, userId); if (realModel is null) { return View(ErrorModel( requestedMode, importSessionId, $"Import Session {importSessionId.Value:N0} could not be found for the current user, or it has no persisted chapter analysis runs yet.")); } return View(realModel); } if (!simulationRequested && runId.HasValue && currentUser.UserId is int legacyUserId) { var realModel = await snapshots.BuildSnapshotAsync(runId.Value, legacyUserId); if (realModel is null) { return View(ErrorModel( requestedMode, null, $"Run {runId.Value:N0} could not be found for the current user. Live visualisation URLs should use importSessionId for book imports.")); } return View(realModel); } if (simulationRequested) { var model = StoryIntelligenceExperiencePrototypeData.Build(); await illustrationLibrary.ApplyApprovedIllustrationsAsync(model); return View(model); } if (currentUser.UserId is int selectorUserId) { var sessions = await snapshots.ListImportSessionOptionsAsync(selectorUserId); return View(new StoryIntelligenceExperiencePrototypeViewModel { Mode = "selector", AvailableRuns = sessions, ProjectName = "Development", BookTitle = "Story Intelligence", SnapshotMessage = sessions.Count == 0 ? "No persisted Story Intelligence import sessions were found for the current user. Start a real import, then refresh this page." : "Choose a Story Intelligence import session, or explicitly open simulation mode.", Diagnostics = new StoryIntelligenceExperienceSnapshotDiagnostics { RequestedMode = requestedMode, RequestedImportSessionId = importSessionId, SnapshotSource = "Real" } }); } return View(ErrorModel(requestedMode, importSessionId, "No development user is signed in.")); } [HttpPost("StoryIntelligenceExperience/Rebuild")] [ValidateAntiForgeryToken] public async Task RebuildStoryIntelligenceExperience( [FromForm] int importSessionId, [FromForm] string? rebuildScope, [FromForm] bool dryRun = false, [FromForm] bool queueDemand = false, [FromForm] int? maxDemandToQueue = null, [FromForm] string? appearancePreference = null) { if (!environment.IsDevelopment()) { return NotFound(); } if (importSessionId <= 0 || currentUser.UserId is not int userId) { return BadRequest("A valid importSessionId is required."); } var rebuild = await storyMemory.RebuildImportSessionAsync( importSessionId, userId, new StoryMemoryRebuildOptions { DryRun = dryRun, RebuildAll = !string.Equals(rebuildScope, "incremental", StringComparison.OrdinalIgnoreCase), QueueDemand = queueDemand, MaxDemandToQueue = maxDemandToQueue, AppearancePreference = string.IsNullOrWhiteSpace(appearancePreference) ? StoryMemoryAppearancePreferences.Balanced : appearancePreference.Trim() }, HttpContext.RequestAborted); if (rebuild.DryRun) { TempData["StoryIntelligenceReplayMessage"] = $"Dry run complete. {rebuild.SceneResultsRead:N0} stored scene result(s), {rebuild.SceneResultsProcessed:N0} parseable. No database changes, AI calls, or image credits."; } else { TempData["StoryIntelligenceReplayMessage"] = $"Durable Story Memory rebuilt. {rebuild.SceneResultsProcessed:N0} scene result(s) processed, {rebuild.AssignmentsCreatedOrValidated:N0} assignment(s), {rebuild.DemandsCreated:N0} demand record(s), {rebuild.AiCalls:N0} AI calls."; } return RedirectToAction(nameof(StoryIntelligenceExperience), new { importSessionId, mode = "replay" }); } [HttpGet("StoryMemoryDiagnostics")] public async Task StoryMemoryDiagnostics([FromQuery] int importSessionId) { if (!environment.IsDevelopment()) { return NotFound(); } if (importSessionId <= 0) { return BadRequest("A valid importSessionId is required."); } var diagnostics = await storyMemoryRepository.GetDiagnosticsAsync(importSessionId); return Json(diagnostics); } [HttpGet("StoryIntelligenceIllustrationDiagnostics")] public async Task StoryIntelligenceIllustrationDiagnostics([FromQuery] int importSessionId) { if (!environment.IsDevelopment()) { return NotFound(); } if (importSessionId <= 0 || currentUser.UserId is not int userId) { return BadRequest("A valid importSessionId is required."); } var model = await snapshots.BuildImportSessionSnapshotAsync(importSessionId, userId); if (model is null) { return NotFound($"Import Session {importSessionId:N0} could not be found for the current user, or it has no persisted chapter analysis runs yet."); } return View(model); } private static StoryIntelligenceExperiencePrototypeViewModel ErrorModel(string requestedMode, int? importSessionId, string message) => new() { Mode = "error", ProjectName = "Development", BookTitle = "Story Intelligence", SnapshotMessage = message, Diagnostics = new StoryIntelligenceExperienceSnapshotDiagnostics { RequestedMode = requestedMode, RequestedImportSessionId = importSessionId, SnapshotSource = "Real", Warning = message } }; }