using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;

namespace ExternalConnectors;

public class PuttyKeyFileGenerator
{
    private const int prefixSize = 4;
    private const int paddedPrefixSize = prefixSize + 1;
    private const int lineLength = 64;
    private const string keyType = "ssh-rsa";
    private const string encryptionType = "none";

    public static string ToPuttyPrivateKey(RSACryptoServiceProvider cryptoServiceProvider, string Comment = "imported-openssh-key")
    {
        var publicParameters = cryptoServiceProvider.ExportParameters(false);
        byte[] publicBuffer = new byte[3 + keyType.Length + GetPrefixSize(publicParameters.Exponent) + publicParameters.Exponent!.Length + GetPrefixSize(publicParameters.Modulus) + publicParameters.Modulus!.Length + 1];

        using (var bw = new BinaryWriter(new MemoryStream(publicBuffer)))
        {
            bw.Write(new byte[] { 0x00, 0x00, 0x00 });
            bw.Write(Encoding.ASCII.GetBytes(keyType));
            PutPrefixed(bw, publicParameters.Exponent, CheckIsNeddPadding(publicParameters.Exponent));
            PutPrefixed(bw, publicParameters.Modulus, CheckIsNeddPadding(publicParameters.Modulus));
        }
        var publicBlob = System.Convert.ToBase64String(publicBuffer);

        var privateParameters = cryptoServiceProvider.ExportParameters(true);

        byte[] privateBuffer = new byte[paddedPrefixSize + privateParameters.D!.Length + paddedPrefixSize + privateParameters.P!.Length + paddedPrefixSize + privateParameters.Q!.Length + paddedPrefixSize + privateParameters.InverseQ!.Length];

        using (var bw = new BinaryWriter(new MemoryStream(privateBuffer)))
        {
            PutPrefixed(bw, privateParameters.D, true);
            PutPrefixed(bw, privateParameters.P, true);
            PutPrefixed(bw, privateParameters.Q, true);
            PutPrefixed(bw, privateParameters.InverseQ, true);
        }
        var privateBlob = System.Convert.ToBase64String(privateBuffer);

        HMACSHA1 hmacSha1 = new(SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes("putty-private-key-file-mac-key")));
        byte[] bytesToHash = new byte[prefixSize + keyType.Length + prefixSize + encryptionType.Length + prefixSize + Comment.Length + prefixSize + publicBuffer.Length + prefixSize + privateBuffer.Length];

        using (var bw = new BinaryWriter(new MemoryStream(bytesToHash)))
        {
            PutPrefixed(bw, Encoding.ASCII.GetBytes(keyType));
            PutPrefixed(bw, Encoding.ASCII.GetBytes(encryptionType));
            PutPrefixed(bw, Encoding.ASCII.GetBytes(Comment));
            PutPrefixed(bw, publicBuffer);
            PutPrefixed(bw, privateBuffer);
        }

        var hash = string.Join("", hmacSha1.ComputeHash(bytesToHash).Select(x => $"{x:x2}"));

        var sb = new StringBuilder();
        sb.AppendLine("PuTTY-User-Key-File-2: " + keyType);
        sb.AppendLine("Encryption: " + encryptionType);
        sb.AppendLine("Comment: " + Comment);

        var publicLines = SpliceText(publicBlob, lineLength);
        sb.AppendLine("Public-Lines: " + publicLines.Length);
        foreach (var line in publicLines)
        {
            sb.AppendLine(line);
        }

        var privateLines = SpliceText(privateBlob, lineLength);
        sb.AppendLine("Private-Lines: " + privateLines.Length);
        foreach (var line in privateLines)
        {
            sb.AppendLine(line);
        }

        sb.AppendLine("Private-MAC: " + hash);

        return sb.ToString();
    }

    private static void PutPrefixed(BinaryWriter bw, byte[] bytes, bool addLeadingNull = false)
    {
        bw.Write(BitConverter.GetBytes(bytes.Length + (addLeadingNull ? 1 : 0)).Reverse().ToArray());
        if (addLeadingNull)
            bw.Write(new byte[] { 0x00 });
        bw.Write(bytes);
    }

    private static string[] SpliceText(string text, int lineLength)
    {
        return Regex.Matches(text, ".{1," + lineLength + "}").Cast<Match>().Select(m => m.Value).ToArray();
    }

    private static int GetPrefixSize(byte[]? bytes)
    {
        if (bytes is null)
            return 0;

        return CheckIsNeddPadding(bytes) ? paddedPrefixSize : prefixSize;
    }

    private static bool CheckIsNeddPadding(byte[] bytes)
    {
        if (bytes is null || bytes.Length == 0)
            return false;

        // 128 == 10000000
        // This means that the number of bits can be divided by 8.
        // According to the algorithm in putty, you need to add a padding.
        return bytes[0] >= 128;
    }
}
