using System.Buffers;
using System.Diagnostics;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using Blake3;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;

namespace StabilityMatrix.Core.Helper;

public static class FileHash
{
    public static async Task<string> GetHashAsync(
        HashAlgorithm hashAlgorithm,
        Stream stream,
        byte[] buffer,
        Action<ulong>? progress = default
    )
    {
        ulong totalBytesRead = 0;

        using (hashAlgorithm)
        {
            int bytesRead;
            while ((bytesRead = await stream.ReadAsync(buffer).ConfigureAwait(false)) != 0)
            {
                totalBytesRead += (ulong)bytesRead;
                hashAlgorithm.TransformBlock(buffer, 0, bytesRead, null, 0);
                progress?.Invoke(totalBytesRead);
            }
            hashAlgorithm.TransformFinalBlock(buffer, 0, 0);
            var hash = hashAlgorithm.Hash;
            if (hash == null || hash.Length == 0)
            {
                throw new InvalidOperationException("Hash algorithm did not produce a hash.");
            }
            return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
        }
    }

    public static async Task<string> GetSha256Async(string filePath, IProgress<ProgressReport>? progress = default)
    {
        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException($"Could not find file: {filePath}");
        }

        var totalBytes = Convert.ToUInt64(new FileInfo(filePath).Length);
        var shared = ArrayPool<byte>.Shared;
        var buffer = shared.Rent((int)FileTransfers.GetBufferSize(totalBytes));
        try
        {
            await using var stream = File.OpenRead(filePath);

            var hash = await GetHashAsync(
                    SHA256.Create(),
                    stream,
                    buffer,
                    totalBytesRead =>
                    {
                        progress?.Report(new ProgressReport(totalBytesRead, totalBytes, type: ProgressType.Hashing));
                    }
                )
                .ConfigureAwait(false);
            return hash;
        }
        finally
        {
            shared.Return(buffer);
        }
    }

    public static async Task<string> GetBlake3Async(string filePath, IProgress<ProgressReport>? progress = default)
    {
        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException($"Could not find file: {filePath}");
        }

        var totalBytes = Convert.ToUInt64(new FileInfo(filePath).Length);
        var readBytes = 0ul;
        var shared = ArrayPool<byte>.Shared;
        var buffer = shared.Rent(GetBufferSize(totalBytes));
        try
        {
            await using var stream = File.OpenRead(filePath);
            using var hasher = Hasher.New();
            while (true)
            {
                var bytesRead = await stream.ReadAsync(buffer).ConfigureAwait(false);
                if (bytesRead == 0)
                {
                    break;
                }
                readBytes += (ulong)bytesRead;
                hasher.Update(buffer.AsSpan(0, bytesRead));
                progress?.Report(new ProgressReport(readBytes, totalBytes));
            }
            return hasher.Finalize().ToString();
        }
        finally
        {
            shared.Return(buffer);
        }
    }

    public static async Task<Hash> GetBlake3Async(Stream stream, IProgress<ProgressReport>? progress = default)
    {
        var totalBytes = Convert.ToUInt64(stream.Length);
        var readBytes = 0ul;
        var shared = ArrayPool<byte>.Shared;
        var buffer = shared.Rent((int)FileTransfers.GetBufferSize(totalBytes));
        try
        {
            using var hasher = Hasher.New();
            while (true)
            {
                var bytesRead = await stream.ReadAsync(buffer).ConfigureAwait(false);
                if (bytesRead == 0)
                {
                    break;
                }
                readBytes += (ulong)bytesRead;
                hasher.Update(buffer.AsSpan(0, bytesRead));
                progress?.Report(new ProgressReport(readBytes, totalBytes));
            }
            return hasher.Finalize();
        }
        finally
        {
            shared.Return(buffer);
        }
    }

    /// <summary>
    /// Get the Blake3 hash of a span of data with multi-threading.
    /// </summary>
    public static Hash GetBlake3Parallel(ReadOnlySpan<byte> data)
    {
        using var hasher = Hasher.New();
        hasher.UpdateWithJoin(data);
        return hasher.Finalize();
    }

    /// <summary>
    /// Task.Run wrapped <see cref="GetBlake3Parallel"/>
    /// </summary>
    public static Task<Hash> GetBlake3ParallelAsync(ReadOnlyMemory<byte> data)
    {
        return Task.Run(() => GetBlake3Parallel(data.Span));
    }

    /// <summary>
    /// Get the Blake3 hash of a file as memory-mapped with multi-threading.
    /// </summary>
    public static Hash GetBlake3MemoryMappedParallel(string filePath)
    {
        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException(filePath);
        }

        var totalBytes = Convert.ToInt64(new FileInfo(filePath).Length);

        using var hasher = Hasher.New();

        // Memory map
        using var fileStream = File.OpenRead(filePath);
        using var memoryMappedFile = MemoryMappedFile.CreateFromFile(
            fileStream,
            null,
            totalBytes,
            MemoryMappedFileAccess.Read,
            HandleInheritability.None,
            false
        );

        using var accessor = memoryMappedFile.CreateViewAccessor(0, totalBytes, MemoryMappedFileAccess.Read);

        Debug.Assert(accessor.Capacity == fileStream.Length);

        var buffer = new byte[accessor.Capacity];
        accessor.ReadArray(0, buffer, 0, buffer.Length);

        hasher.UpdateWithJoin(buffer);
        return hasher.Finalize();
    }

    /// <summary>
    /// Task.Run wrapped <see cref="GetBlake3MemoryMappedParallel"/>
    /// </summary>
    public static Task<Hash> GetBlake3MemoryMappedParallelAsync(string filePath)
    {
        return Task.Run(() => GetBlake3MemoryMappedParallel(filePath));
    }

    /// <summary>
    /// Determines suitable buffer size for hashing based on stream length.
    /// </summary>
    private static int GetBufferSize(ulong totalBytes) =>
        totalBytes switch
        {
            < Size.MiB => 8 * (int)Size.KiB,
            < 500 * Size.MiB => 16 * (int)Size.KiB,
            < Size.GiB => 32 * (int)Size.KiB,
            _ => 64 * (int)Size.KiB
        };
}
