using Metin2Launcher.Logging;

namespace Metin2Launcher.Apply;

/// <summary>
/// Moves staged files into their final locations.
///
/// Design note: this is NOT a single-transaction atomic apply over the whole set.
/// Each individual file is moved atomically via <see cref="File.Move(string, string, bool)"/>
/// (which on .NET 8 uses MoveFileEx with MOVEFILE_REPLACE_EXISTING on Windows, and
/// rename(2) on Unix — both atomic at the per-file level). If the process dies mid-apply,
/// the player ends up with a partially-applied state.
///
/// Acceptable for MVP because:
///   - content-addressed blobs are idempotent, so a restart re-applies what's left
///   - the sha256 walk on next launch detects and heals a partial apply
/// A proper "all or nothing" apply would require shadow directory + rename-of-dir,
/// which the manifest design explicitly does not (yet) demand.
/// </summary>
public static class AtomicApplier
{
    public readonly record struct StagedFile(string StagingPath, string FinalPath);

    /// <summary>Applies all staged files. Throws on the first failing move; already-applied files stay applied.</summary>
    public static void Apply(IEnumerable<StagedFile> files)
    {
        foreach (var f in files)
        {
            ApplyOne(f.StagingPath, f.FinalPath);
        }
    }

    public static void ApplyOne(string stagingPath, string finalPath)
    {
        if (!File.Exists(stagingPath))
            throw new FileNotFoundException("staging file not found", stagingPath);

        var dir = Path.GetDirectoryName(finalPath);
        if (!string.IsNullOrEmpty(dir))
            Directory.CreateDirectory(dir);

        // File.Move with overwrite:true is atomic on both Windows (MoveFileEx) and Unix (rename(2))
        // as long as source and destination are on the same filesystem — which they are when
        // staging lives under client/.updates/staging.
        File.Move(stagingPath, finalPath, overwrite: true);
        Log.Info($"applied {finalPath}");
    }
}
