using System.IO;
using System.IO.Compression;
using System.Net.Http;
using ytLive.Helpers;

namespace ytLive.Services.Encoder;

/// <summary>
/// The default <see cref="IFfmpegLocator"/>: probe PATH first (the user's own
/// install wins), then the cache in <c>%APPDATA%\ytLlive\tools</c>, then pull the
/// pinned BtbN **lgpl-shared** build (TASK 4 ship step 2). The shared variant is a
/// deliberate licensing choice: dynamic linking means LGPL compliance is "license
/// text + source offer", with no static-relink (LGPL §6) material required. The
/// shared zip puts <c>ffmpeg.exe</c> plus the <c>libav*.dll</c> family in <c>bin/</c>,
/// so both are extracted — Windows resolves the DLLs from the exe's own directory.
/// Search dirs, tools dir, and the downloader are constructor-injected so tests
/// fake the network and stay on a temp directory.
/// </summary>
public sealed class FfmpegLocator : IFfmpegLocator
{
    public const string FileName = "ffmpeg.exe";

    /// <summary>Pinned BtbN LGPL-shared win64 build (immutable autobuild tag; see TASKS.md).</summary>
    public const string PinnedUrl =
        "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-08-09-13-03/ffmpeg-master-latest-win64-lgpl-shared.zip";

    private readonly string[] _searchDirs;
    private readonly string _toolsDir;
    private readonly Func<string, CancellationToken, Task<byte[]>> _downloader;

    public FfmpegLocator(
        string[]? searchDirs = null,
        string? toolsDir = null,
        Func<string, CancellationToken, Task<byte[]>>? downloader = null)
    {
        _searchDirs = searchDirs ?? ParsePath();
        _toolsDir = toolsDir ?? Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ytLlive", "tools");
        _downloader = downloader ?? DefaultDownload;
    }

    public async Task<string> LocateAsync(CancellationToken cancellationToken = default)
    {
        foreach (var dir in _searchDirs)
        {
            if (string.IsNullOrEmpty(dir)) continue;
            var candidate = Path.Combine(dir, FileName);
            if (File.Exists(candidate)) return candidate;
        }

        var cached = Path.Combine(_toolsDir, FileName);
        if (File.Exists(cached) && new FileInfo(cached).Length > 0) return cached;

        try
        {
            var zip = await _downloader(PinnedUrl, cancellationToken).ConfigureAwait(false);
            if (zip.Length == 0)
                throw new IOException($"FFmpeg download from {PinnedUrl} returned an empty payload.");
            Directory.CreateDirectory(_toolsDir);
            ExtractBinaries(zip, _toolsDir);
            return cached;
        }
        catch (Exception ex) when (ex is IOException or InvalidDataException or NotSupportedException)
        {
            AppLog.Write(ex, "FFmpeg locator: download/extract failed");
            throw;
        }
    }

    /// <summary>
    /// Extract <c>ffmpeg.exe</c> and every <c>*.dll</c> into <c>toolsDir</c> via a
    /// staging directory, so a failed extract never leaves a partially-populated
    /// cache behind (the previous good cache stays until every move succeeds).
    /// </summary>
    private static void ExtractBinaries(byte[] zip, string toolsDir)
    {
        var staging = toolsDir + ".stage";
        if (Directory.Exists(staging)) Directory.Delete(staging, recursive: true);
        Directory.CreateDirectory(staging);
        try
        {
            using var stream = new MemoryStream(zip, writable: false);
            using var archive = new ZipArchive(stream, ZipArchiveMode.Read);
            var exe = archive.Entries.FirstOrDefault(
                    e => e.FullName.EndsWith("/" + FileName, StringComparison.OrdinalIgnoreCase))
                ?? throw new InvalidDataException($"The pinned FFmpeg archive does not contain {FileName}.");
            ExtractOne(exe, Path.Combine(staging, FileName));
            foreach (var entry in archive.Entries)
            {
                if (entry.FullName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
                    ExtractOne(entry, Path.Combine(staging, Path.GetFileName(entry.FullName)));
            }
            foreach (var file in Directory.GetFiles(staging))
                File.Move(file, Path.Combine(toolsDir, Path.GetFileName(file)), overwrite: true);
        }
        finally
        {
            if (Directory.Exists(staging)) Directory.Delete(staging, recursive: true);
        }
    }

    private static void ExtractOne(ZipArchiveEntry entry, string destination)
    {
        var temp = destination + ".tmp";
        try
        {
            using (var source = entry.Open())
            using (var target = File.Create(temp))
                source.CopyTo(target);
            File.Move(temp, destination);
        }
        finally
        {
            if (File.Exists(temp)) File.Delete(temp);
        }
    }

    private static async Task<byte[]> DefaultDownload(string url, CancellationToken ct)
    {
        using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
        return await http.GetByteArrayAsync(url, ct).ConfigureAwait(false);
    }

    private static string[] ParsePath()
    {
        var raw = Environment.GetEnvironmentVariable("PATH") ?? "";
        return raw.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
    }
}
