using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using CmlLib.Core;
using CmlLib.Core.Auth;
using CmlLib.Core.Installers;
using CmlLib.Core.ProcessBuilder;
using CmlLib.Core.Installer.Forge;
using CmlLib.Core.Installer.NeoForge;
using ModpackLauncher.Models;

namespace ModpackLauncher.Services;

public sealed class LaunchService
{
    private readonly MinecraftLauncher _launcher;
    private readonly ForgeInstaller _forgeInstaller;
    private readonly NeoForgeInstaller _neoForgeInstaller;

    private readonly string _installDir;

    public LaunchService(string installDir)
    {
        _installDir = installDir;
        var path = new MinecraftPath(installDir);
        _launcher = new MinecraftLauncher(path);
        _forgeInstaller = new ForgeInstaller(_launcher);
        _neoForgeInstaller = new NeoForgeInstaller(_launcher);
    }

    public MinecraftLauncher Launcher => _launcher;

    /// <summary>
    /// Belt-and-braces check after the loader installer runs: parse the version JSON
    /// and download any libraries that are listed but missing on disk. Works around
    /// a known CmlLib quirk where libraries with @jar suffix in the Maven coordinate
    /// (e.g. "cpw.mods:bootstraplauncher:2.0.2@jar" used by NeoForge 21.1.x) get
    /// skipped by the installer's library downloader.
    /// </summary>
    private async Task VerifyVersionLibrariesAsync(string versionId,
                                                    IProgress<ProgressReport> progress,
                                                    CancellationToken ct)
    {
        var versionJsonPath = Path.Combine(_installDir, "versions", versionId, $"{versionId}.json");
        if (!File.Exists(versionJsonPath)) return;

        using var doc = JsonDocument.Parse(await File.ReadAllTextAsync(versionJsonPath, ct).ConfigureAwait(false));
        if (!doc.RootElement.TryGetProperty("libraries", out var libsArr)) return;

        var libsDir = Path.Combine(_installDir, "libraries");
        var missing = new System.Collections.Generic.List<(string Path, string Url, string? Sha1)>();

        foreach (var lib in libsArr.EnumerateArray())
        {
            if (!lib.TryGetProperty("downloads", out var dls)) continue;
            if (!dls.TryGetProperty("artifact", out var art)) continue;
            if (!art.TryGetProperty("path", out var pPath) || !art.TryGetProperty("url", out var pUrl)) continue;

            var relPath = pPath.GetString();
            var url = pUrl.GetString();
            var sha1 = art.TryGetProperty("sha1", out var pSha1) ? pSha1.GetString() : null;
            if (string.IsNullOrEmpty(relPath) || string.IsNullOrEmpty(url)) continue;

            var fullPath = Path.Combine(libsDir, relPath.Replace('/', Path.DirectorySeparatorChar));
            if (!File.Exists(fullPath))
            {
                missing.Add((fullPath, url, sha1));
            }
        }

        if (missing.Count == 0) return;

        progress.Report(new ProgressReport(
            ProgressKind.Status,
            $"Verifying loader libraries: {missing.Count} missing, fetching..."));

        using var http = new HttpClient();
        http.Timeout = TimeSpan.FromMinutes(5);
        for (int i = 0; i < missing.Count; i++)
        {
            ct.ThrowIfCancellationRequested();
            var (path, url, _) = missing[i];
            progress.Report(new ProgressReport(
                ProgressKind.Log,
                $"  Library {i + 1}/{missing.Count}: {Path.GetFileName(path)}"));
            Directory.CreateDirectory(Path.GetDirectoryName(path)!);
            var bytes = await http.GetByteArrayAsync(url, ct).ConfigureAwait(false);
            await File.WriteAllBytesAsync(path, bytes, ct).ConfigureAwait(false);
        }

        progress.Report(new ProgressReport(
            ProgressKind.Status,
            "Loader libraries verified."));
    }

    public async Task<string> InstallVersionAsync(
        Manifest manifest,
        IProgress<ProgressReport> progress,
        CancellationToken ct)
    {
        EventHandler<InstallerProgressChangedEventArgs> fileProgressHandler = (_, args) =>
        {
            var pct = args.TotalTasks > 0
                ? (args.ProgressedTasks * 100.0 / args.TotalTasks)
                : -1;
            progress.Report(new ProgressReport(
                ProgressKind.Progress,
                $"{args.EventType}: {args.Name ?? ""}",
                Current: args.ProgressedTasks,
                Total: args.TotalTasks,
                Percent: pct
            ));
        };

        EventHandler<ByteProgress> byteProgressHandler = (_, args) =>
        {
            if (args.TotalBytes <= 0) return;
            var pct = args.ToRatio() * 100.0;
            progress.Report(new ProgressReport(
                ProgressKind.Progress,
                $"{args.ProgressedBytes:N0} / {args.TotalBytes:N0} bytes",
                Current: (int)Math.Min(args.ProgressedBytes, int.MaxValue),
                Total: (int)Math.Min(args.TotalBytes, int.MaxValue),
                Percent: pct
            ));
        };

        _launcher.FileProgressChanged += fileProgressHandler;
        _launcher.ByteProgressChanged += byteProgressHandler;

        try
        {
            var mcVersion = manifest.Minecraft.Version;
            var loader = manifest.Loader;

            progress.Report(new ProgressReport(ProgressKind.Status, $"Installing Minecraft {mcVersion}..."));
            await _launcher.InstallAsync(mcVersion, ct).AsTask().ConfigureAwait(false);

            if (loader == null || string.IsNullOrEmpty(loader.Type) ||
                loader.Type.Equals("vanilla", StringComparison.OrdinalIgnoreCase))
            {
                return mcVersion;
            }

            if (loader.Type.Equals("forge", StringComparison.OrdinalIgnoreCase))
            {
                progress.Report(new ProgressReport(
                    ProgressKind.Status,
                    $"Installing Forge {loader.Version} for {mcVersion}..."
                ));
                var fid = await _forgeInstaller.Install(mcVersion, loader.Version).ConfigureAwait(false);
                await VerifyVersionLibrariesAsync(fid, progress, ct).ConfigureAwait(false);
                return fid;
            }

            if (loader.Type.Equals("neoforge", StringComparison.OrdinalIgnoreCase))
            {
                progress.Report(new ProgressReport(
                    ProgressKind.Status,
                    $"Installing NeoForge {loader.Version} for {mcVersion}..."
                ));
                var nid = await _neoForgeInstaller.Install(mcVersion, loader.Version).ConfigureAwait(false);
                await VerifyVersionLibrariesAsync(nid, progress, ct).ConfigureAwait(false);
                return nid;
            }

            throw new NotSupportedException(
                $"Loader '{loader.Type}' is not yet supported. Use 'forge', 'neoforge', or 'vanilla'."
            );
        }
        finally
        {
            _launcher.FileProgressChanged -= fileProgressHandler;
            _launcher.ByteProgressChanged -= byteProgressHandler;
        }
    }

    public async Task<int> LaunchAsync(
        string versionId,
        MSession session,
        int minMemoryMB,
        int maxMemoryMB,
        IProgress<ProgressReport> progress,
        CancellationToken ct)
    {
        var option = new MLaunchOption
        {
            Session = session,
            MaximumRamMb = maxMemoryMB,
            MinimumRamMb = minMemoryMB,
            // Generational ZGC: low-pause concurrent collector -- recommended by
            // Distant Horizons (and broadly better than the default G1 for modded MC).
            // Requires Java 21+ which CmlLib auto-installs for MC 1.21.1.
            ExtraJvmArguments = new[]
            {
                new MArgument("-XX:+UseZGC"),
                new MArgument("-XX:+ZGenerational"),
            }
        };

        progress.Report(new ProgressReport(ProgressKind.Status, "Building launch process..."));
        var process = await _launcher.BuildProcessAsync(versionId, option).ConfigureAwait(false);

        var wrapper = new ProcessWrapper(process);
        wrapper.OutputReceived += (_, log) =>
        {
            progress.Report(new ProgressReport(ProgressKind.Log, log));
        };

        wrapper.StartWithEvents();
        progress.Report(new ProgressReport(
            ProgressKind.Status,
            $"Minecraft launched (PID {process.Id})"
        ));

        var exitCode = await wrapper.WaitForExitTaskAsync().ConfigureAwait(false);
        progress.Report(new ProgressReport(
            ProgressKind.Status,
            $"Game exited (code {exitCode})"
        ));
        return exitCode;
    }
}
