// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

#nullable enable

namespace Microsoft.Workload.Build.Tasks
{
    internal sealed class PackageInstaller
    {
        private readonly string _tempDir;
        private string _nugetConfigContents;
        private TaskLoggingHelper _logger;
        private string _packagesDir;
        private bool _cleanPackagesdir;

        private PackageInstaller(string nugetConfigContents, string baseTempDir, string? packagesPath, TaskLoggingHelper logger)
        {
            _nugetConfigContents = nugetConfigContents;

            _cleanPackagesdir = packagesPath is null;
            _logger = logger;
            _tempDir = Path.Combine(baseTempDir, Path.GetRandomFileName());
            _packagesDir = packagesPath ?? Path.Combine(_tempDir, "nuget-packages");
        }

        public static bool Install(PackageReference[] references, string nugetConfigContents, string baseTempDir, TaskLoggingHelper logger, bool stopOnMissing=true, string? packagesPath=null)
        {
            if (references.Length == 0)
                return true;

            return new PackageInstaller(nugetConfigContents, baseTempDir, packagesPath, logger)
                        .InstallActual(references, stopOnMissing);
        }

        private bool InstallActual(PackageReference[] references, bool stopOnMissing)
        {
            // Restore packages
            if (_cleanPackagesdir && Directory.Exists(_packagesDir))
            {
                _logger.LogMessage(MessageImportance.Low, $"Deleting {_packagesDir}");
                Directory.Delete(_packagesDir, recursive: true);
            }

            var projecDir = Path.Combine(_tempDir, "restore");
            var projectPath = Path.Combine(projecDir, "Restore.csproj");

            Directory.CreateDirectory(projecDir);

            File.WriteAllText(Path.Combine(projecDir, "Directory.Build.props"), """
<Project>

  <!-- This is an empty Directory.Build.props file to prevent projects which reside
       under this directory to use any of the repository local settings. -->
  <PropertyGroup>
    <ImportDirectoryPackagesProps>false</ImportDirectoryPackagesProps>
    <ImportDirectoryBuildTargets>false</ImportDirectoryBuildTargets>
  </PropertyGroup>

</Project>
""");
            File.WriteAllText(projectPath, GenerateProject(references));
            File.WriteAllText(Path.Combine(projecDir, "nuget.config"), _nugetConfigContents);

            _logger.LogMessage(MessageImportance.Low, $"Restoring packages: {string.Join(", ", references.Select(r => $"{r.Name}/{r.Version}"))}");

            string args = $"restore \"{projectPath}\" /p:RestorePackagesPath=\"{_packagesDir}\" /bl:{Path.Combine(_tempDir, "restore.binlog")}";
            (int exitCode, string output) = Utils.TryRunProcess(_logger, "dotnet", args, silent: false, debugMessageImportance: MessageImportance.Low);
            if (exitCode != 0)
            {
                LogErrorOrWarning($"Restoring packages failed with exit code: {exitCode}. Output:{Environment.NewLine}{output}", stopOnMissing);
                return false;
            }

            List<(PackageReference, string)> failedToRestore = references
                                                             .Select(r => (r, Path.Combine(_packagesDir, r.Name.ToLowerInvariant(), r.Version)))
                                                             .Where(tuple => !Directory.Exists(tuple.Item2))
                                                             .ToList();

            if (failedToRestore.Count > 0)
            {
                _logger.LogMessage(MessageImportance.Normal, output);
                foreach ((PackageReference pkgRef, string pkgDir) in failedToRestore)
                    LogErrorOrWarning($"Could not restore {pkgRef.Name}/{pkgRef.Version} (can't find {pkgDir})", stopOnMissing);

                return false;
            }

            return LayoutPackages(references, stopOnMissing);
        }

        private bool LayoutPackages(IEnumerable<PackageReference> references, bool stopOnMissing)
        {
            foreach (var pkgRef in references)
            {
                var source = Path.Combine(_packagesDir, pkgRef.Name.ToLowerInvariant(), pkgRef.Version, pkgRef.relativeSourceDir);
                if (!Directory.Exists(source))
                {
                    LogErrorOrWarning($"Failed to restore {pkgRef.Name}/{pkgRef.Version} (could not find {source})", stopOnMissing);
                    if (stopOnMissing)
                        return false;
                }
                else
                {
                    if (!CopyDirectoryAfresh(source, pkgRef.OutputDir) && stopOnMissing)
                        return false;
                }
            }

            return true;
        }

        private static string GenerateProject(IEnumerable<PackageReference> references)
        {
            StringBuilder projectFileBuilder = new();
            projectFileBuilder.Append(@"
<Project Sdk=""Microsoft.NET.Sdk"">
    <PropertyGroup>
        <TargetFramework>net$(NETCoreAppMaximumVersion)</TargetFramework>
    </PropertyGroup>
    <ItemGroup>");

            foreach (var reference in references)
                projectFileBuilder.AppendLine($"<PackageDownload Include=\"{reference.Name}\" Version=\"[{reference.Version}]\" />");

            projectFileBuilder.Append(@"
    </ItemGroup>
</Project>
");

            return projectFileBuilder.ToString();
        }

        private bool CopyDirectoryAfresh(string srcDir, string destDir)
        {
            try
            {
                if (Directory.Exists(destDir))
                {
                    _logger.LogMessage(MessageImportance.Low, $"Deleting {destDir}");
                    Directory.Delete(destDir, recursive: true);
                }

                _logger.LogMessage(MessageImportance.Low, $"Copying {srcDir} to {destDir}");
                Directory.CreateDirectory(destDir);
                Utils.DirectoryCopy(srcDir, destDir);

                return true;
            }
            catch (Exception ex)
            {
                _logger.LogError($"Failed while copying {srcDir} => {destDir}: {ex.Message}");
                if (ex is IOException)
                    return false;

                throw;
            }
        }

        private void LogErrorOrWarning(string msg, bool stopOnMissing)
        {
            if (stopOnMissing)
                _logger.LogError(msg);
            else
                _logger.LogWarning(msg);
        }
    }
}
