using System.IO;
using System.Reflection;
using System.Security.Cryptography;

namespace AmiReel.Services;

public sealed record ToolPaths(string Ffmpeg, string Ffprobe);

public static class ToolExtractor
{
    public static async Task<ToolPaths> ResolveAsync(string ffmpegPath, string ffprobePath, CancellationToken cancellationToken = default)
    {
        if (!string.IsNullOrWhiteSpace(ffmpegPath) || !string.IsNullOrWhiteSpace(ffprobePath))
        {
            if (string.IsNullOrWhiteSpace(ffmpegPath) || !File.Exists(ffmpegPath))
                throw new FileNotFoundException("Configured ffmpeg.exe was not found.");
            if (string.IsNullOrWhiteSpace(ffprobePath) || !File.Exists(ffprobePath))
                throw new FileNotFoundException("Configured ffprobe.exe was not found.");
            return new ToolPaths(ffmpegPath, ffprobePath);
        }

        if (TryResolveFromSystemPath(out ToolPaths? systemTools) && systemTools is not null)
            return systemTools;

        return await ExtractAsync(cancellationToken);
    }

    public static bool TryResolveFromSystemPath(out ToolPaths? tools)
    {
        tools = null;
        string? ffmpeg = FindOnPath("ffmpeg.exe");
        string? ffprobe = FindOnPath("ffprobe.exe");
        if (ffmpeg is null || ffprobe is null)
            return false;

        tools = new ToolPaths(ffmpeg, ffprobe);
        return true;
    }

    private static async Task<ToolPaths> ExtractAsync(CancellationToken cancellationToken = default)
    {
        string toolDir = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "AmiReel", "tools", "0.1.0");
        Directory.CreateDirectory(toolDir);

        string ffmpeg = await ExtractOneAsync("ffmpeg.exe", toolDir, cancellationToken);
        string ffprobe = await ExtractOneAsync("ffprobe.exe", toolDir, cancellationToken);
        return new ToolPaths(ffmpeg, ffprobe);
    }

    private static async Task<string> ExtractOneAsync(string fileName, string destination, CancellationToken token)
    {
        Assembly assembly = Assembly.GetExecutingAssembly();
        string resourceName = $"AmiReel.Tools.{fileName}";
        await using Stream source = assembly.GetManifestResourceStream(resourceName)
            ?? throw new InvalidOperationException(
                $"Embedded {fileName} was not found. Add it to ThirdParty and publish the application again.");

        string target = Path.Combine(destination, fileName);
        string temporary = target + ".new";
        await using (FileStream output = File.Create(temporary))
            await source.CopyToAsync(output, token);

        if (File.Exists(target) && FilesMatch(target, temporary))
        {
            File.Delete(temporary);
            return target;
        }

        File.Move(temporary, target, true);
        return target;
    }

    private static bool FilesMatch(string first, string second)
    {
        using SHA256 sha = SHA256.Create();
        using FileStream a = File.OpenRead(first);
        byte[] aHash = sha.ComputeHash(a);
        using FileStream b = File.OpenRead(second);
        byte[] bHash = sha.ComputeHash(b);
        return aHash.AsSpan().SequenceEqual(bHash);
    }

    private static string? FindOnPath(string fileName)
    {
        string? path = Environment.GetEnvironmentVariable("PATH");
        if (string.IsNullOrWhiteSpace(path))
            return null;

        foreach (string directory in path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
        {
            try
            {
                string candidate = Path.Combine(directory, fileName);
                if (File.Exists(candidate))
                    return candidate;
            }
            catch
            {
            }
        }

        return null;
    }
}
