using System.Text.Json; using PlotLine.Data; using PlotLine.Models; namespace PlotLine.Services; public interface IProjectRestoreService { Task RestoreAsync(int projectId, int projectRestorePointId); } public sealed class ProjectRestoreService( IProjectRestorePointRepository restorePointRepository, IProjectRestorePointService restorePointService, ILogger logger) : IProjectRestoreService { private static readonly JsonSerializerOptions ReadOptions = new() { PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }; private static readonly JsonSerializerOptions RestoreSqlOptions = new() { WriteIndented = false }; public async Task RestoreAsync(int projectId, int projectRestorePointId) { var restorePoint = await restorePointRepository.GetAsync(projectRestorePointId, projectId); if (restorePoint?.JsonData is null) { return Failure("Restore point could not be found."); } ProjectBackupExport? backup; try { backup = JsonSerializer.Deserialize(restorePoint.JsonData, ReadOptions); } catch (JsonException ex) { logger.LogWarning(ex, "Restore point {ProjectRestorePointID} contains invalid JSON.", projectRestorePointId); return Failure("Restore point JSON is invalid. No project data was changed."); } var validationMessage = ValidateBackup(projectId, backup); if (validationMessage is not null) { return Failure(validationMessage); } var safetyBackup = await restorePointService.CreatePreRestoreAsync(projectId, restorePoint.RestorePointName); if (safetyBackup is null) { logger.LogError("PreRestore backup creation failed for project {ProjectID}. Restore was aborted.", projectId); return Failure("A safety backup could not be created. Restore was aborted and no project data was changed."); } try { var restoreJson = JsonSerializer.Serialize(backup, RestoreSqlOptions); await restorePointRepository.RestoreAsync(projectId, projectRestorePointId, safetyBackup.ProjectRestorePointID, restoreJson); logger.LogInformation("Project {ProjectID} restored from restore point {ProjectRestorePointID}. Safety backup {SafetyRestorePointID} was created first.", projectId, projectRestorePointId, safetyBackup.ProjectRestorePointID); return new ProjectRestoreResult { Succeeded = true, Message = "Project restored. A safety backup of the previous state was created first.", SafetyBackup = safetyBackup }; } catch (Exception ex) { logger.LogError(ex, "Project restore failed for project {ProjectID} from restore point {ProjectRestorePointID}. Safety backup {SafetyRestorePointID} was retained.", projectId, projectRestorePointId, safetyBackup.ProjectRestorePointID); return new ProjectRestoreResult { Succeeded = false, Message = "Restore failed. The safety backup was created, and the project data was left unchanged.", SafetyBackup = safetyBackup }; } } private static string? ValidateBackup(int projectId, ProjectBackupExport? backup) { if (backup is null) { return "Restore point JSON is invalid. No project data was changed."; } if (!string.Equals(backup.Format, "PlotDirector.ProjectBackup", StringComparison.Ordinal)) { return "Restore point format is not recognised. No project data was changed."; } if (!string.Equals(backup.FormatVersion, "1.0", StringComparison.Ordinal)) { return "Restore point format version is not supported. No project data was changed."; } if (backup.Project?.ProjectID != projectId) { return "This restore point does not belong to the selected project. No project data was changed."; } return null; } private static ProjectRestoreResult Failure(string message) => new() { Succeeded = false, Message = message }; }