#if DEBUG
using System.Formats.Tar;
using System.IO.Compression;
using System.Text.Json;
using Duplicati.Library.Logging;
using Duplicati.Library.Utility;

namespace Duplicati.WebserverCore.Middlewares;

internal static class NpmSpaHelper
{
    private static readonly string LOGTAG = Log.LogTagFromType(typeof(NpmSpaHelper));

    private sealed record PackageJson(Dictionary<string, string>? dependencies, string? packageId);
    private sealed record PackageLockJson(Dictionary<string, PackageInfo>? packages);
    private sealed record PackageInfo(string version, string resolved, string? integrity);
    private sealed record InstalledPackageJson(string version);
    public sealed record SpaConfig(FileInfo IndexFile, string BasePath);

    public static SpaConfig? InstallNpmPackage(string packageUrl, string targetPath)
    {
        var tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());

        try
        {
            // Download and extract the package
            Directory.CreateDirectory(tempPath);

            var tgzFile = Path.Combine(tempPath, "package.tgz");
            var tarFile = Path.ChangeExtension(tgzFile, ".tar");
            using (var client = new HttpClient())
            {
                var response = client.GetAsync(packageUrl).Await();
                response.EnsureSuccessStatusCode();

                using (var stream = response.Content.ReadAsStream())
                using (var fileStream = File.Create(tgzFile))
                    stream.CopyTo(fileStream);
            }

            // Extract the zipped contents
            using (var fsource = new FileStream(tgzFile, FileMode.Open, FileAccess.Read))
            using (var ftarget = new FileStream(tarFile, FileMode.Create, FileAccess.Write))
            using (var gzip = new GZipStream(new FileStream(tgzFile, FileMode.Open, FileAccess.Read), CompressionMode.Decompress))
                gzip.CopyTo(ftarget);

            var dest = Path.Combine(tempPath, "package");

            // Extract the tar entries into the temp folder
            using (var fsource = new FileStream(tarFile, FileMode.Open, FileAccess.Read))
            using (var reader = new TarReader(fsource))
                while (reader.GetNextEntry() is TarEntry tar)
                {
                    var path = Path.Combine(tempPath, tar.Name);

                    // Ensure the path is within the temp folder
                    if (!path.StartsWith(dest))
                        continue;

                    if (tar.EntryType == TarEntryType.Directory)
                        Directory.CreateDirectory(path);
                    else if (tar.EntryType == TarEntryType.RegularFile)
                    {
                        Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Invalid path"));
                        tar.ExtractToFile(path, false);
                    }
                }

            // Read the package.json and check it looks correct
            var packageJson = JsonSerializer.Deserialize<InstalledPackageJson>(File.ReadAllText(Path.Combine(dest, "package.json")));
            if (packageJson == null || string.IsNullOrWhiteSpace(packageJson.version))
                return null;

            // Make room for the new package
            if (Directory.Exists(targetPath))
                Directory.Delete(targetPath, true);

            var parentFolder = Path.GetDirectoryName(targetPath);
            if (parentFolder != null && !Directory.Exists(parentFolder))
                Directory.CreateDirectory(parentFolder);

            // Move the package to the target path
            Directory.Move(dest, targetPath);

            // Clean up
            Directory.Delete(tempPath, true);

            return new SpaConfig(new FileInfo(Path.Combine(targetPath, "index.html")), targetPath);
        }
        catch (Exception ex)
        {
            Library.Logging.Log.WriteErrorMessage(LOGTAG, "SpaDebugHelperFail", ex, "NPM SPA package installation failed");
        }
        finally
        {
            if (Directory.Exists(tempPath))
                Directory.Delete(tempPath, true);
        }

        return null;
    }

    /// <summary>
    /// Probes for a missing/outdates SPA in the target folder and installs it if found.
    /// </summary>
    /// <param name="basepath">The base path to probe</param>
    /// <returns>The SPA configuration if found, otherwise null</returns>
    public static SpaConfig? ProbeForNpmSpa(string basepath)
    {
        try
        {
            // Check if the package.json and package-lock.json files exist
            var packageFile = Path.Combine(basepath, "package.json");
            var packagelockFile = Path.Combine(basepath, "package-lock.json");

            if (!File.Exists(packageFile) || !File.Exists(packagelockFile))
                return null;

            var packageJson = JsonSerializer.Deserialize<PackageJson>(File.ReadAllText(packageFile));
            var packageLockJson = JsonSerializer.Deserialize<PackageLockJson>(File.ReadAllText(packagelockFile));

            var packageId = string.IsNullOrWhiteSpace(packageJson?.packageId) ? packageJson?.dependencies?.Keys.FirstOrDefault() : packageJson.packageId;
            if (string.IsNullOrWhiteSpace(packageId) || packageJson?.dependencies == null)
                return null;

            var version = packageJson.dependencies.GetValueOrDefault(packageId);
            if (string.IsNullOrWhiteSpace(version))
                return null;

            // Remove version prefix
            if (!char.IsAsciiDigit(version.First()))
                version = version[1..];

            var packageUrl = packageLockJson?.packages?.GetValueOrDefault($"node_modules/{packageId}")?.resolved;
            if (string.IsNullOrWhiteSpace(packageUrl))
                return null;

            // Package is not installed, install it
            var packageFolder = Path.Combine(basepath, "node_modules", packageId);
            if (!Directory.Exists(packageFolder))
                return InstallNpmPackage(packageUrl, packageFolder);

            // The installed package is missing version, reinstall
            var installedPackageJson = Path.Combine(packageFolder, "package.json");
            if (!File.Exists(installedPackageJson))
                return InstallNpmPackage(packageUrl, packageFolder);

            // The installed package is not the correct version, reinstall
            var installedJson = JsonSerializer.Deserialize<InstalledPackageJson>(File.ReadAllText(installedPackageJson));
            if (installedJson == null || string.IsNullOrWhiteSpace(installedJson.version) || installedJson.version != version)
                return InstallNpmPackage(packageUrl, packageFolder);

            // Package is installed and correct version
            return new SpaConfig(new FileInfo(Path.Combine(packageFolder, "index.html")), packageFolder);
        }
        catch (Exception ex)
        {
            Library.Logging.Log.WriteErrorMessage(LOGTAG, "SpaDebugHelperFail", ex, "NPM SPA package installation failed");
        }

        return null;
    }

}
#endif