using System.Collections;
using System.Diagnostics;
using Renci.SshNet;
using Spectre.Console;
using Spectre.Console.Cli;
using VsTools.Projects;

namespace  aeqw89.tools.Publish;

/*
 * Structure of the program:
 *     - publish (executable)
 *         - overwrite
 *             - destinations
 *                 - flags
 *         - increment
 *             - patch|minor|major
 *                 - destinations
 *                 - flags
 * e.g. publish overwrite|increment [patch|minor|major] destinations [flags]
 */

public static class Program {
    public static Mode Mode { get; set; }
    public static IncrementTarget? Target { get; set; }
    public static string[] Destinations { get; set; }
    public static Dictionary<string, string[]> Flags { get; set; }
    public static bool Verbose { get; set; } = false;

    public static void ReadArgs(string[] args) {
        if (args.Length < 1) {
            ShowError(Exceptions.missing_mode.EscapeMarkup());
            ShowHelp();
            return;
        }
        
        Mode = args[0] switch {
            "overwrite" => Mode.Overwrite,
            "increment" => Mode.Increment,
            _ => (Mode)(-1)
        };
        
        if (Mode == (Mode)(-1)) {
            ShowError(Exceptions.could_not_parse_mode.EscapeMarkup(), args[0].EscapeMarkup());
            ShowHelp();
            return;
        }
        
        if (args.Length < 2) {
            if (Mode == Mode.Increment)
                ShowError(Exceptions.missing_increment_target.EscapeMarkup()); 
            else if (Mode == Mode.Overwrite)
                ShowError(Exceptions.missing_destinations.EscapeMarkup());
            ShowHelp();
            return;           
        }
        
        Destinations = args[1..];
        Flags = [];
        if (Mode == Mode.Increment) {
            if (args.Length < 3) {
                ShowError(Exceptions.missing_destinations.EscapeMarkup());
                ShowHelp();
                return;           
            }
            
            Destinations = args[2..];
            
            Target = args[1] switch {
                "patch" => IncrementTarget.Patch,
                "minor" => IncrementTarget.Minor,
                "major" => IncrementTarget.Major,
                _ => (IncrementTarget)(-1)
            };

            if (Target == (IncrementTarget)(-1)) {
                ShowError(Exceptions.could_not_parse_target.EscapeMarkup(), args[1].EscapeMarkup());
                ShowHelp();
                return;
            }
        }
        
        string? firstFlag = Destinations.FirstOrDefault(x => x.StartsWith('-'));
        if (firstFlag == null) return;
        string[] flags = Destinations.SkipWhile(x => x != firstFlag).ToArray();
        Flags = ReadFlags(flags);
        Destinations = Destinations.TakeWhile(x => x != firstFlag).ToArray();
        Verbose = Flags.ContainsKey("--verbose") || Flags.ContainsKey("-v");
    }

    private static Dictionary<string, string[]> ReadFlags(string[] flags) {
        Dictionary<string, string[]> result = [];
        List<string> collected = [];
        string lastKey = flags[0];
        if (flags.Length == 1)
            result[lastKey] = [];
        foreach (var flag in flags.Skip(1)) {
            if (flag.StartsWith('-')) {
                result[lastKey] = collected.ToArray();
                collected = [];
                lastKey = flag;
            } else
                collected.Add(flag);
        }

        return result;
    }

    public static async Task Main(string[] args) {
        ReadArgs(args);

        string packageId = "";
        string version = "";

        var result = AnsiConsole.Status()
            .Spinner(Spinner.Known.Dots)
            .Start<bool>("Preparing project", ctx => {
                ctx.Status = "Locating project file";
                if (!ProjectFile.TryLoad(Environment.CurrentDirectory, out var projectFile, out var error)) {
                    ShowError(error.EscapeMarkup());
                    return false;
                }

                packageId = projectFile.GetPackageId();
                
                try {
                    projectFile.Backup();
                    if (Verbose)
                        AnsiConsole.WriteLine(
                            $"Created project file backup at {projectFile.GetDefaultBackupLocation()}");

                    ctx.Status = "Repairing project file";
                    if (!Flags.ContainsKey("--skip-repair"))
                        if (!projectFile.TryRepair(out error)) {
                            ShowError(error.EscapeMarkup());
                            projectFile.Restore();
                            return false;
                        }

                    if (Mode == Mode.Increment && !Flags.ContainsKey("--simulate")) {
                        int delta = 1;
                        if (Flags.TryGetValue("--delta", out var deltaStrings)) {
                            if (deltaStrings.Length != 1) {
                                ShowError(Exceptions.flag_parameter_length_incorrect.EscapeMarkup(), "--delta", 1,
                                    deltaStrings.Length);
                                projectFile.Restore();
                                ShowHelp();
                                return false;
                            }

                            if (!int.TryParse(deltaStrings[0], out delta)) {
                                ShowError(Exceptions.flag_parameter_type_incorrect.EscapeMarkup(), "--delta", 0, nameof(Int32),
                                    deltaStrings[0]);
                                projectFile.Restore();
                                ShowHelp();
                                return false;
                            }
                        }

                        ctx.Status = "Updating version";
                        var version = projectFile.GetVersion();
                        version = ChangeVersion(version,
                            Target == IncrementTarget.Patch ? delta : 0,
                            Target == IncrementTarget.Minor ? delta : 0,
                            Target == IncrementTarget.Major ? delta : 0,
                            (x, y) => x + y);

                        projectFile.SetVersion(version);
                    }
                }
                catch (Exception e) {
                    ShowError(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup());
                    projectFile.Restore();
                    return false;
                }

                version = projectFile.GetVersion();

                if (!Flags.ContainsKey("--simulate")) {
                    try {
                        var packageReferences = projectFile.GetPackageReferences();
                        foreach (var reference in packageReferences.Where(x => !projectFile.IsTransitive(x)))
                            projectFile.SetPrivateAssets(reference, PrivateAssetsValue.All);
                        foreach (var reference in packageReferences.Where(x => projectFile.IsTransitive(x)))
                            projectFile.RemovePackage(reference);

                        HashSet<string> visited = [];
                        var projectReferences = new Queue<Item>(projectFile.GetProjectReferences().Cast<Item>());
                        while (projectReferences.Count != 0) {
                            var reference = projectReferences.Dequeue();
                            visited.Add(reference.Include);

                            if (Verbose)
                                AnsiConsole.WriteLine($"Processing project reference {reference.Include} out of {visited.Count} so far");
                            
                            projectFile.SetPrivateAssets(reference, PrivateAssetsValue.All);
                            string pathToReferencedProjectFile = projectFile.GetAbsoluteIncludePath(reference);
                            if (!ProjectFile.TryLoad(pathToReferencedProjectFile, out var referencedProjectFile,
                                    out error)) {
                                ShowError(error.EscapeMarkup());
                                projectFile.Restore();
                                return false;
                            }

                            var referencedPackageReferences = referencedProjectFile.GetPackageReferences();
                            foreach (var package in referencedPackageReferences) {
                                if (Verbose)
                                    AnsiConsole.WriteLine($"Hoisting package {package.Include} from {pathToReferencedProjectFile}");
                                var hoisted = projectFile.AddPackage(package);
                                projectFile.SetTransitive(hoisted, true);
                                projectFile.SetPrivateAssets(hoisted, PrivateAssetsValue.None);
                                referencedProjectFile.SetPrivateAssets(package, PrivateAssetsValue.All);
                            }

                            var referencedProjectReferences = referencedProjectFile.GetProjectReferences();
                            foreach (var project in referencedProjectReferences) {
                                if (!visited.Contains(project.Include))
                                    projectReferences.Enqueue(project);
                            }
                        }
                    } catch (Exception e) {
                        ShowError(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup());
                        projectFile.Restore();
                        return false;
                    }
                }

                projectFile.Save();
                return true;
            });

        if (!result) {
            return;
        }

        var outDir = Path.GetRandomFileName();
        result = AnsiConsole.Status()
            .Spinner(Spinner.Known.Dots)
            .Start<bool>("Creating package with 'dotnet pack' ", ctx => {
                var p = Process.Start(new ProcessStartInfo() {
                    FileName = "dotnet",
                    Arguments = $"pack -o {outDir}",
                    WorkingDirectory = Environment.CurrentDirectory,
                    UseShellExecute = Verbose,
                    RedirectStandardOutput = !Verbose,
                    RedirectStandardError = !Verbose
                });
                p?.WaitForExit();
                return p?.ExitCode == 0;
            });

        if (!result) {
            ShowError(Exceptions.dotnet_pack_failure.EscapeMarkup());
            return;
        }
        
        var package = Directory.GetFiles(outDir, "*.nupkg").FirstOrDefault();
        if (package == null) {
            ShowError(Exceptions.generic_error.EscapeMarkup());
            return;
        }

        var inMemory = await File.ReadAllBytesAsync(package);
        var size = new FileInfo(package).Length;
        const long bufferSize = 80 * 1024; // 80 KB
        try {
            await AnsiConsole.Progress()
                .AutoClear(true)
                .HideCompleted(true)
                .Columns(new ProgressColumn[] {
                    new TaskDescriptionColumn(),
                    new ProgressBarColumn()
                        .RemainingStyle(Style.Parse("dim gray slowblink"))
                        .CompletedStyle(Style.Parse("green strikethrough"))
                        .FinishedStyle("green strikethrough"),
                    new DownloadedColumn(),
                    new RemainingTimeColumn(),
                    new TransferSpeedColumn(),
                })
                .StartAsync(async ctx => {
                    await Parallel.ForEachAsync(Destinations, new ParallelOptions() {
                        MaxDegreeOfParallelism = Environment.ProcessorCount,
                    }, async (dest, ct) => {
                        using var reader = new MemoryStream(inMemory);
                        var task = ctx.AddTask(dest, new ProgressTaskSettings() {
                            MaxValue = size
                        });

                        if (dest.StartsWith("local-")) {
                            var name = dest[("local-".Length)..];
                            var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
                                name, Path.GetFileName(package));
                            if (!Directory.Exists(Path.GetDirectoryName(path)))
                                Directory.CreateDirectory(Path.GetDirectoryName(path)!);
                            await using var writer = File.OpenWrite(path);
                            var buffer = new byte[bufferSize];
                            int read;
                            do {
                                read = await reader.ReadAsync(buffer, ct);
                                writer.Write(buffer, 0, read);
                                task.Increment(read);
                            } while (read > 0);
                        }

                        else if (dest.StartsWith("cloud-")) {
                            var name = dest[("cloud-".Length)..];
                            var connectionTask = ctx.AddTaskBefore($"Preparing cloud-{name}", new ProgressTaskSettings() {
                                MaxValue = 100
                            }, task);

                            if (!SshHosts.TryGetHost(name, out var host)) {
                                ShowError(Exceptions.cloud_host_not_found.EscapeMarkup(), name);
                                return;
                            }

                            var connectionInfo = SshHosts.GetConnection(name);
                            using var sshClient = new SshClient(connectionInfo);
                            if (!sshClient.IsConnected)
                                await sshClient.ConnectAsync(ct);
                            connectionTask.Increment(33);
                            
                            var winC = sshClient.RunCommand("cmd /c ver");
                            var othC = sshClient.RunCommand("uname -s");

                            var os = (winC.ExitStatus, othC.ExitStatus) switch {
                                (0, _) => "windows",
                                (_, 0) => "linux",
                                _ => "unknown"
                            };

                            string remoteDirectory;
                            string packageFileDirectory;
                            if (os == "windows") {
                                var userDirC = sshClient.RunCommand("cmd /c echo %USERPROFILE%");
                                if (userDirC.ExitStatus != 0) {
                                    ShowError(Exceptions.failed_to_prepare_server_directory, "n/a", name, os, userDirC.Result);
                                    return;
                                }

                                var userDir = userDirC.Result.Trim();
                                remoteDirectory = RemotePath.Combine(RemoteOs.Windows,userDir, "dotnet-packages");
                                packageFileDirectory = RemotePath.Combine(RemoteOs.Windows, remoteDirectory, Path.GetFileName(package));

                                var mkdirC = sshClient.RunCommand($"cmd /c if not exist \"{remoteDirectory}\" mkdir \"{remoteDirectory}\"");
                                if (mkdirC.ExitStatus != 0) {
                                    ShowError(Exceptions.failed_to_prepare_server_directory, remoteDirectory, name, os, mkdirC.Result);
                                    return;
                                }
                            }
                            else if (os == "linux") {
                                var homeDirC = sshClient.RunCommand("printf %s \"$HOME\"");
                                if (homeDirC.ExitStatus != 0) {
                                    ShowError(Exceptions.failed_to_prepare_server_directory, "n/a", name, os, homeDirC.Result);
                                    return;
                                }
                                var homeDir = homeDirC.Result.Trim(); // no CRLF on unix, but Trim() is safest
                                remoteDirectory = RemotePath.Combine(RemoteOs.Unix, homeDir, ".dotnet-packages");
                                packageFileDirectory = RemotePath.Combine(RemoteOs.Unix, remoteDirectory, Path.GetFileName(package));

                                // Use -p and single quotes to handle spaces safely
                                var mkdirC = sshClient.RunCommand($"mkdir -p '{remoteDirectory}'");
                                if (mkdirC.ExitStatus != 0) {
                                    ShowError(Exceptions.failed_to_prepare_server_directory, remoteDirectory, name, os, mkdirC.Result);
                                    return;
                                }
                            }
                            else {
                                ShowError(Exceptions.failed_to_prepare_server_directory, "n/a", name, os, "Unsupported OS");
                                return;
                            }
                            connectionTask.Increment(33);
                            
                            sshClient.Disconnect();
                            
                            using var client = new SftpClient(connectionInfo);
                            if (!client.IsConnected)
                                await client.ConnectAsync(ct);
                            connectionTask.Increment(33);
                            connectionTask.StopTask();

                            await using var writer = client.OpenWrite(packageFileDirectory);
                            byte[] buffer = new byte[bufferSize];
                            int read;
                            do {
                                read = await reader.ReadAsync(buffer, ct);
                                writer.Write(buffer, 0, read);
                                task.Increment(read);
                            } while (read > 0);
                        }
                        
                        else if (dest == "github") {
                            var p = Process.Start(new ProcessStartInfo() {
                                FileName = "dotnet",
                                Arguments = $"nuget push {package} --source github",
                                WorkingDirectory = Environment.CurrentDirectory,
                                UseShellExecute = false,
                                RedirectStandardOutput = !Verbose,
                                RedirectStandardError = !Verbose
                            });
                            
                            if (p == null) {
                                ShowError(Exceptions.generic_error.EscapeMarkup());
                            }

                            task.Increment(size / 2);
                            if (p != null)
                                await p.WaitForExitAsync(ct);
                            if (p?.ExitCode != 0) {
                                ShowError(Exceptions.dotnet_nuget_push_failure, p.ExitCode);
                            }
                            task.Increment(size / 2);
                        }
                        
                        task.StopTask();
                    });
                });
        }
        finally {
            try {
                Directory.Delete(outDir, true);
            }
            catch (Exception e) {
                ShowError(string.Format(Exceptions.failed_to_clean_up.EscapeMarkup(), outDir.EscapeMarkup(), e.ToString().EscapeMarkup()));
            }
        }
        AnsiConsole.MarkupLine("Completed processing of all destinations.");
        AnsiConsole.MarkupLine("Example usage:\n\t <PackageReference Include=\"{0}\" Version=\"{1}\" />".EscapeMarkup(), packageId, version);
    }

    /// <summary>
    /// Updates the version string by applying the specified operation to the major, minor, and patch components of the version.
    /// </summary>
    /// <param name="version">The current version string in the format "major.minor.patch[-tag]".</param>
    /// <param name="patch">The value to apply to the patch component.</param>
    /// <param name="minor">The value to apply to the minor component.</param>
    /// <param name="major">The value to apply to the major component.</param>
    /// <param name="operation">A function that defines the adjustment operation to be performed on each version component.</param>
    /// <returns>A new version string with the updated major, minor, and patch components, preserving any existing tag.</returns>
    /// <exception cref="Exception">Thrown if the version string is not in the correct format.</exception>
    private static string ChangeVersion(string version, int patch, int minor, int major,
        Func<int, int, int> operation) {
        string[] split = version.Split('.');
        if (split.Length != 3) {
            throw new Exception(string.Format(Exceptions.version_string_not_formatted_correctly, version));
        }
        
        string tag = "";
        if (split[2].Contains('-')) {
            var split2 = split[2].Split('-');
            split[2] = split2[0];
            tag = "-" + split2[1];
        }
        
        if (split.Any(x => !int.TryParse(x, out _)))
            throw new Exception(string.Format(Exceptions.version_string_not_formatted_correctly, version));
        
        int[] parsedVersion = split.Select(int.Parse).ToArray();

        return
            $"{operation(parsedVersion[0], major)}.{operation(parsedVersion[1], minor)}.{operation(parsedVersion[2], patch)}{tag}";
    }
    
    private static void ShowError(string message, params object[] args) {
        AnsiConsole.MarkupLine($"[bold red]{message}[/]", args);
    }
    
    private static void ShowHelp() {
        AnsiConsole.Markup(("Usage: publish overwrite|increment [patch|minor|major] destinations [flags]\n" +
                           "\t if mode: overwrite destinations [flags]\n" +
                           "\t if mode: increment patch|minor|major [flags]\n").EscapeMarkup());
        
    }
}