// Required namespaces:
// System, System.IO, System.Linq, System.Text, System.Text.RegularExpressions, System.Collections.Generic, Renci.SshNet

using System.Text;
using System.Text.RegularExpressions;

namespace aeqw89.tools.Publish;

public record Host(
    string Name,
    string Hostname,
    string User,
    string Password,
    int Port,
    List<string> IdentityFiles,
    Renci.SshNet.ProxyTypes? ProxyType,
    string? ProxyHost,
    int? ProxyPort,
    string? ProxyUser,
    string? ProxyPassword
);

public static class SshHosts {
    public static List<Host> Hosts { get; set; }

    static SshHosts() {
        Hosts = new List<Host>();

        var path = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
            ".ssh",
            "config"
        );

        if (!File.Exists(path)) return;

        var lines = File.ReadAllLines(path);

        string[]? currentNames = null;
        string? currentHostName = null;
        string? currentUser = null;
        string? currentPassword = null;
        int? currentPort = null;
        List<string>? currentIdentityFiles = null;

        // Proxy (explicit)
        Renci.SshNet.ProxyTypes? currentProxyType = null;
        string? currentProxyHost = null;
        int? currentProxyPort = null;
        string? currentProxyUser = null;
        string? currentProxyPassword = null;

        // Proxy (derived from ProxyCommand)
        string? currentProxyCommand = null;

        void ResetBlock()
        {
            currentNames = null;
            currentHostName = null;
            currentUser = null;
            currentPassword = null;
            currentPort = null;
            currentIdentityFiles = null;

            currentProxyType = null;
            currentProxyHost = null;
            currentProxyPort = null;
            currentProxyUser = null;
            currentProxyPassword = null;

            currentProxyCommand = null;
        }

        void MaybeDeriveProxyFromProxyCommand()
        {
            if (string.IsNullOrWhiteSpace(currentProxyCommand)) return;
            if (currentProxyType != null && currentProxyHost != null && currentProxyPort != null) return;

            // Very common forms:
            //   nc -x host:port -X 5 %h %p         (SOCKS5)
            //   nc -x host:port -X 4 %h %p         (SOCKS4)
            //   nc -x host:port %h %p              (default: treat as SOCKS5)
            //   nc -X connect -x host:port %h %p   (HTTP CONNECT)
            //   connect -S host:port %h %p         (treat as SOCKS5)
            var s = currentProxyCommand;

            // host:port extraction
            var hp = Regex.Match(s, @"(?:(?:-x|-S)\s+|\s)([A-Za-z0-9.\-]+):(\d{1,5})");
            if (hp.Success) {
                currentProxyHost ??= hp.Groups[1].Value;
                if (int.TryParse(hp.Groups[2].Value, out var pp) && pp > 0 && pp <= 65535)
                    currentProxyPort ??= pp;
            }

            // type extraction
            if (Regex.IsMatch(s, @"-X\s+5\b", RegexOptions.IgnoreCase))
                currentProxyType ??= Renci.SshNet.ProxyTypes.Socks5;
            else if (Regex.IsMatch(s, @"-X\s+4\b", RegexOptions.IgnoreCase))
                currentProxyType ??= Renci.SshNet.ProxyTypes.Socks4;
            else if (Regex.IsMatch(s, @"-X\s+connect\b", RegexOptions.IgnoreCase))
                currentProxyType ??= Renci.SshNet.ProxyTypes.Http;
            else if (s.Contains("connect ", System.StringComparison.OrdinalIgnoreCase))
                currentProxyType ??= Renci.SshNet.ProxyTypes.Http;
            else if (s.Contains("nc ", System.StringComparison.OrdinalIgnoreCase))
                currentProxyType ??= Renci.SshNet.ProxyTypes.Socks5;

            // If we still don't have enough, leave proxy unset.
        }

        void Flush()
        {
            if (currentNames == null || currentNames.Length == 0) {
                ResetBlock();
                return;
            }

            // Try deriving proxy from ProxyCommand if explicit values weren't set
            MaybeDeriveProxyFromProxyCommand();

            foreach (var n in currentNames) {
                var hn = string.IsNullOrWhiteSpace(currentHostName) ? n : currentHostName!;
                var idFiles = new List<string>();
                if (currentIdentityFiles != null) {
                    foreach (var f in currentIdentityFiles) idFiles.Add(ExpandPath(f));
                }

                Hosts.Add(new Host(
                    Name: n,
                    Hostname: hn,
                    User: currentUser ?? string.Empty,
                    Password: currentPassword ?? string.Empty,
                    Port: currentPort ?? 22,
                    IdentityFiles: idFiles,
                    ProxyType: currentProxyType,
                    ProxyHost: currentProxyHost,
                    ProxyPort: currentProxyPort,
                    ProxyUser: currentProxyUser,
                    ProxyPassword: currentProxyPassword
                ));
            }

            ResetBlock();
        }

        foreach (var raw in lines) {
            var line = raw.Trim();
            if (line.Length == 0 || line.StartsWith("#")) continue;

            int idx = line.IndexOfAny(new[] { ' ', '\t' });
            string key, value;
            if (idx < 0) {
                key = line;
                value = string.Empty;
            } else {
                key = line[..idx];
                value = line[idx..].Trim();
            }

            switch (key.ToLowerInvariant()) {
                case "host":
                    Flush();
                    currentNames = SplitArgs(value).ToArray();
                    break;

                case "hostname":
                    if (currentNames != null) currentHostName = value;
                    break;

                case "user":
                    if (currentNames != null) currentUser = value;
                    break;

                case "password":
                    // Non-standard; supported here as a convenience (used for password auth or key passphrase)
                    if (currentNames != null) currentPassword = value;
                    break;

                case "port":
                    if (currentNames != null && int.TryParse(value, out var p) && p > 0 && p <= 65535)
                        currentPort = p;
                    break;

                case "identityfile":
                    if (currentNames != null) {
                        currentIdentityFiles ??= new List<string>();
                        foreach (var f in SplitArgs(value)) {
                            if (!string.IsNullOrWhiteSpace(f)) currentIdentityFiles.Add(f);
                        }
                    }
                    break;

                // --- Proxy settings (explicit custom keys) ---
                case "proxytype":
                    if (currentNames != null) {
                        var v = value.ToLowerInvariant();
                        currentProxyType =
                            v switch {
                                "socks5" or "socks" or "socks5h" => Renci.SshNet.ProxyTypes.Socks5,
                                "socks4" => Renci.SshNet.ProxyTypes.Socks4,
                                "http" or "https" or "connect" => Renci.SshNet.ProxyTypes.Http,
                                _ => currentProxyType
                            };
                    }
                    break;

                case "proxyhost":
                    if (currentNames != null) currentProxyHost = value;
                    break;

                case "proxyport":
                    if (currentNames != null && int.TryParse(value, out var prxP) && prxP > 0 && prxP <= 65535)
                        currentProxyPort = prxP;
                    break;

                case "proxyuser":
                    if (currentNames != null) currentProxyUser = value;
                    break;

                case "proxypassword":
                    if (currentNames != null) currentProxyPassword = value;
                    break;

                // --- OpenSSH-style hints we try to interpret ---
                case "proxycommand":
                    if (currentNames != null) currentProxyCommand = value;
                    break;

                // (Note: ProxyJump is non-trivial to replicate in SSH.NET; not parsed here.)

                default:
                    break;
            }
        }

        Flush();
    }

    // Builds a ConnectionInfo from a parsed host, ensuring all ~/.ssh private keys are tried.
    private static Renci.SshNet.ConnectionInfo BuildConnection(Host h) {
        var authMethods = new List<Renci.SshNet.AuthenticationMethod>();

        // 1) Collect candidate key file paths: from config + everything that looks like a private key in ~/.ssh
        var keyPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var f in h.IdentityFiles)
            if (!string.IsNullOrWhiteSpace(f))
                keyPaths.Add(f);

        foreach (var f in EnumerateAllSshPrivateKeys())
            keyPaths.Add(f);

        // 2) Load keys (try with passphrase, then without)
        var pkFiles = new List<Renci.SshNet.PrivateKeyFile>();
        foreach (var p in keyPaths) {
            if (!File.Exists(p)) continue;
            try {
                if (!string.IsNullOrEmpty(h.Password)) {
                    try {
                        pkFiles.Add(new Renci.SshNet.PrivateKeyFile(p, h.Password));
                        continue;
                    }
                    catch { /* fall through to try without passphrase */
                    }
                }

                pkFiles.Add(new Renci.SshNet.PrivateKeyFile(p));
            }
            catch { /* skip unreadable/unparsable key */
            }
        }

        if (pkFiles.Count > 0)
            authMethods.Add(new Renci.SshNet.PrivateKeyAuthenticationMethod(h.User, pkFiles.ToArray()));

        // 3) Optional password auth (remote account password)
        if (!string.IsNullOrEmpty(h.Password))
            authMethods.Add(new Renci.SshNet.PasswordAuthenticationMethod(h.User, h.Password));

        if (authMethods.Count == 0)
            throw new InvalidOperationException($"No authentication methods available for host '{h.Name}'.");

        // 4) Proxy-aware ConnectionInfo
        if (h.ProxyType.HasValue && !string.IsNullOrWhiteSpace(h.ProxyHost) && h.ProxyPort.HasValue) {
            return new Renci.SshNet.ConnectionInfo(
                h.Hostname,
                h.Port,
                h.User,
                h.ProxyType.Value,
                h.ProxyHost!,
                h.ProxyPort.Value,
                h.ProxyUser,
                h.ProxyPassword,
                authMethods.ToArray()
            );
        }

        return new Renci.SshNet.ConnectionInfo(h.Hostname, h.Port, h.User, authMethods.ToArray());
    }

    // Enumerate everything in ~/.ssh that *looks* like a private key.
    // Skips obvious non-keys like *.pub, known_hosts, config, and directories.
    private static IEnumerable<string> EnumerateAllSshPrivateKeys() {
        var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
        var sshDir = Path.Combine(home, ".ssh");
        if (!Directory.Exists(sshDir)) yield break;

        // Common private key basenames to prioritize
        var preferred = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
            "id_rsa", "id_ecdsa", "id_ed25519", "id_dsa"
        };

        // First yield preferred names if present
        foreach (var name in preferred) {
            var p = Path.Combine(sshDir, name);
            if (File.Exists(p)) yield return p;
        }

        // Then yield everything else that looks like a key
        foreach (var file in Directory.EnumerateFiles(sshDir)) {
            var name = Path.GetFileName(file);

            // Skip ones we already yielded
            if (preferred.Contains(name)) continue;

            // Exclude common non-key files and public keys
            if (name.EndsWith(".pub", StringComparison.OrdinalIgnoreCase)) continue;
            if (name.Equals("config", StringComparison.OrdinalIgnoreCase)) continue;
            if (name.Equals("known_hosts", StringComparison.OrdinalIgnoreCase)) continue;
            if (name.Equals("authorized_keys", StringComparison.OrdinalIgnoreCase)) continue;
            if (name.EndsWith(".crt", StringComparison.OrdinalIgnoreCase)) continue;
            if (name.EndsWith(".csr", StringComparison.OrdinalIgnoreCase)) continue;
            if (name.EndsWith(".pem.pub", StringComparison.OrdinalIgnoreCase)) continue; // guard for odd combos
            if (name.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) continue;
            if (name.EndsWith(".conf", StringComparison.OrdinalIgnoreCase)) continue;

            // Heuristic: accept files with no extension, or with .key/.pem
            var ext = Path.GetExtension(name);
            if (string.IsNullOrEmpty(ext) ||
                ext.Equals(".key", StringComparison.OrdinalIgnoreCase) ||
                ext.Equals(".pem", StringComparison.OrdinalIgnoreCase)) {
                yield return file;
            }
        }
    }


    public static Host Get(string name) {
        for (int i = 0; i < Hosts.Count; i++) {
            if (string.Equals(Hosts[i].Name, name, StringComparison.OrdinalIgnoreCase)) {
                return Hosts[i];
            }
        }
        throw new KeyNotFoundException($"SSH host '{name}' not found.");
    }

    public static bool TryGetHost(string name, out Host host) {
        for (int i = 0; i < Hosts.Count; i++) {
            if (string.Equals(Hosts[i].Name, name, StringComparison.OrdinalIgnoreCase)) {
                host = Hosts[i];
                return true;
            }
        }
        host = default!;
        return false;
    }

    public static Renci.SshNet.ConnectionInfo GetConnection(string name)
    {
        var h = Get(name);
        return BuildConnection(h);
    }

    public static bool TryGetConnection(string name, out Renci.SshNet.ConnectionInfo connectionInfo)
    {
        if (TryGetHost(name, out var h)) {
            try {
                connectionInfo = BuildConnection(h);
                return true;
            } catch {
                // If auth material is unusable, return false.
            }
        }
        connectionInfo = default!;
        return false;
    }

    // --- Helpers ---

    // Splits a value into whitespace-delimited tokens while respecting double/single quotes.
    static IEnumerable<string> SplitArgs(string input)
    {
        if (string.IsNullOrWhiteSpace(input)) yield break;

        bool inSingle = false, inDouble = false;
        var sb = new StringBuilder();

        for (int i = 0; i < input.Length; i++) {
            char c = input[i];

            if (c == '"' && !inSingle) { inDouble = !inDouble; continue; }
            if (c == '\'' && !inDouble) { inSingle = !inSingle; continue; }

            if (!inSingle && !inDouble && char.IsWhiteSpace(c)) {
                if (sb.Length > 0) { yield return sb.ToString(); sb.Clear(); }
            } else {
                sb.Append(c);
            }
        }
        if (sb.Length > 0) yield return sb.ToString();
    }

    // Expands leading "~" to user home. Leaves %h, %r, etc. untouched.
    static string ExpandPath(string p)
    {
        if (string.IsNullOrEmpty(p)) return p;
        if (p == "~") return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
        if (p.StartsWith("~/")) {
            var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
            return Path.Combine(home, p[2..]);
        }
        return p;
    }
}
