#nullable enable using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using LANCommander.SDK.Migrations; using Microsoft.Extensions.Logging; namespace LANCommander.SDK.Services; /// /// Service for tracking and persisting migration history to a JSON file. /// public class MigrationHistoryService(ILogger logger) { private const string HistoryFileName = "migration-history.json"; private readonly string _historyFilePath = AppPaths.GetConfigPath(HistoryFileName); private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; private MigrationHistory? _history; /// /// Loads the migration history from disk. /// public async Task LoadHistoryAsync() { if (_history is not null) return _history; if (!File.Exists(_historyFilePath)) { logger.LogInformation("No migration history file found at {Path}, creating new history", _historyFilePath); _history = new MigrationHistory(); return _history; } try { var json = await File.ReadAllTextAsync(_historyFilePath); _history = JsonSerializer.Deserialize(json, _jsonOptions) ?? new MigrationHistory(); logger.LogInformation("Loaded migration history with {Count} entries", _history.Entries.Count); } catch (Exception ex) { logger.LogWarning(ex, "Failed to load migration history from {Path}, starting with empty history", _historyFilePath); _history = new MigrationHistory(); } return _history; } /// /// Saves the migration history to disk. /// public async Task SaveHistoryAsync() { if (_history is null) return; try { _history.LastUpdated = DateTimeOffset.UtcNow; var json = JsonSerializer.Serialize(_history, _jsonOptions); var directory = Path.GetDirectoryName(_historyFilePath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) Directory.CreateDirectory(directory); await File.WriteAllTextAsync(_historyFilePath, json); logger.LogDebug("Saved migration history to {Path}", _historyFilePath); } catch (Exception ex) { logger.LogError(ex, "Failed to save migration history to {Path}", _historyFilePath); } } /// /// Checks if a migration has already been successfully executed. /// public async Task HasMigrationBeenExecutedAsync(IMigration migration) { var history = await LoadHistoryAsync(); var migrationName = migration.GetType().FullName ?? migration.GetType().Name; return history.Entries.Any(e => e.MigrationName == migrationName && e.Status == MigrationStatus.Executed); } /// /// Records the result of a migration attempt. /// public async Task RecordMigrationAsync( IMigration migration, MigrationStatus status, long durationMs, bool shouldExecute, bool? preChecksPassed = null, Exception? exception = null) { var history = await LoadHistoryAsync(); var entry = new MigrationHistoryEntry { MigrationName = migration.GetType().FullName ?? migration.GetType().Name, Version = migration.Version.ToString(), Status = status, ExecutedAt = DateTimeOffset.UtcNow, DurationMs = durationMs, ShouldExecute = shouldExecute, PreChecksPassed = preChecksPassed, ErrorMessage = exception?.Message, StackTrace = exception?.StackTrace, ApplicationVersion = GetApplicationVersion(), Application = GetApplicationName(), MachineName = Environment.MachineName }; history.Entries.Add(entry); await SaveHistoryAsync(); logger.LogInformation( "Recorded migration {MigrationName} with status {Status}", entry.MigrationName, entry.Status); } /// /// Gets all history entries for a specific migration. /// public async Task> GetMigrationHistoryAsync(string migrationName) { var history = await LoadHistoryAsync(); return [.. history.Entries .Where(e => e.MigrationName == migrationName) .OrderByDescending(e => e.ExecutedAt)]; } /// /// Gets all successfully executed migrations. /// public async Task> GetExecutedMigrationsAsync() { var history = await LoadHistoryAsync(); return [.. history.Entries .Where(e => e.Status == MigrationStatus.Executed) .OrderBy(e => e.ExecutedAt)]; } private static string? GetApplicationVersion() { try { var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); var version = assembly.GetName().Version; return version?.ToString(); } catch { return null; } } private static string? GetApplicationName() { try { var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); return assembly.GetName().Name; } catch { return null; } } }