using Org.BouncyCastle.Asn1;
using System;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace Hcs.ClientNet.Api.Request
{
    internal class X509Tools
    {
        private const string PRIVATE_KEY_USAGE_PERIOD = "2.5.29.16";

        public static string GetFullnameWithExpirationDateStr(X509Certificate2 x509cert)
        {
            var (фамилия, имя, отчество) = GetFullname(x509cert);
            return (string.IsNullOrEmpty(фамилия) ? "" : $"{фамилия} ") + (string.IsNullOrEmpty(имя) ? "" : $"{имя} ") +
                (string.IsNullOrEmpty(отчество) ? "" : $"{отчество} ") + "until " + GetNotAfterDate(x509cert).ToString("dd.MM.yyyy");
        }

        /// <summary>
        /// Возвращает массив из трех строк, содержащих соответственно Фамилию, Имя и Отчество
        /// полученных из данных сертификата. Если сертификат не содержит ФИО, то возвращается массив
        /// из трех пустых строк. Это не точный метод определять имя, он предполагает, что
        /// поля SN, G, CN содержат ФИО в определенном порядке, что правдоподобно но не обязательно.
        /// </summary>
        private static (string Фамилия, string Имя, string Отчество) GetFullname(X509Certificate2 x509cert)
        {
            string фам = "", имя = "", отч = "";

            // Сначала ищем поля surname (SN) и given-name (G)
            var sn = DecodeSubjectField(x509cert, "SN");
            var g = DecodeSubjectField(x509cert, "G");
            if (!string.IsNullOrEmpty(sn) && !string.IsNullOrEmpty(g))
            {
                фам = sn;

                var gParts = g.Split(' ');
                if (gParts != null && gParts.Length >= 1)
                {
                    имя = gParts[0];
                }
                if (gParts != null && gParts.Length >= 2)
                {
                    отч = gParts[1];
                }
            }
            else
            {
                // Иначе берем три первых слова из common name (CN), игнорируя кавычки
                var cn = DecodeSubjectField(x509cert, "CN");
                if (!string.IsNullOrEmpty(cn))
                {
                    cn = new StringBuilder(cn).Replace("\"", "").ToString();

                    char[] separators = [' ', ';'];
                    var cnParts = cn.Split(separators);
                    if (cnParts != null && cnParts.Length >= 1)
                    {
                        фам = cnParts[0];
                    }
                    if (cnParts != null && cnParts.Length >= 2)
                    {
                        имя = cnParts[1];
                    }
                    if (cnParts != null && cnParts.Length >= 3)
                    {
                        отч = cnParts[2];
                    }
                }
            }

            return (фам, имя, отч);
        }

        /// <summary>
        /// Возвращает значение поля с именем @subName включенного в различимое имя Subject
        /// </summary>
        private static string DecodeSubjectField(X509Certificate2 x509cert, string subName)
        {
            // Чтобы посмотреть все поля сертификата
            //System.Diagnostics.Trace.WriteLine("x509decode = " + x509cert.SubjectName.Decode(
            //X500DistinguishedNameFlags.UseNewLines));

            // Декодируем различимое имя на отдельные строки через переводы строк для надежности разбора
            var decoded = x509cert.SubjectName.Decode(X500DistinguishedNameFlags.UseNewLines);
            char[] separators = ['\n', '\r'];
            var parts = decoded.Split(separators, StringSplitOptions.RemoveEmptyEntries);
            if (parts == null)
            {
                return null;
            }

            // Каждая часть начинается с имени и отделяется от значения символом равно
            foreach (var part in parts)
            {
                if (part.Length <= subName.Length + 1)
                {
                    continue;
                }

                if (part.StartsWith(subName) && part[subName.Length] == '=')
                {
                    return part.Substring(subName.Length + 1);
                }
            }

            return null;
        }

        /// <summary>
        /// Возвращает дату окончания действия сертификата
        /// </summary>
        private static DateTime GetNotAfterDate(X509Certificate2 x509cert)
        {
            // Сначала пытаемся определить срок первичного ключа, а затем уже самого сертификата
            var датаОкончания = GetPrivateKeyUsageEndDate(x509cert);
            if (датаОкончания != null)
            {
                return (DateTime)датаОкончания;
            }
            return x509cert.NotAfter;
        }

        private static DateTime? GetPrivateKeyUsageEndDate(X509Certificate2 x509cert)
        {
            foreach (var ext in x509cert.Extensions)
            {
                if (ext.Oid.Value == PRIVATE_KEY_USAGE_PERIOD)
                {
                    // Дата начала с индексом 0, дата окончания с индексом 1
                    return ParseAsn1Datetime(ext, 1);
                }
            }
            return null;
        }

        /// <summary>
        /// Разбирает значение типа дата из серии значений ASN1 присоединенных к расширению
        /// </summary>
        private static DateTime? ParseAsn1Datetime(X509Extension ext, int valueIndex)
        {
            try
            {
                var asnObject = (new Asn1InputStream(ext.RawData)).ReadObject();
                if (asnObject == null)
                {
                    return null;
                }

                var asnSequence = Asn1Sequence.GetInstance(asnObject);
                if (asnSequence.Count <= valueIndex)
                {
                    return null;
                }

                var asn = (Asn1TaggedObject)asnSequence[valueIndex];
                var asnStr = Asn1OctetString.GetInstance(asn, false);
                var s = Encoding.UTF8.GetString(asnStr.GetOctets());
                var year = int.Parse(s.Substring(0, 4));
                var month = int.Parse(s.Substring(4, 2));
                var day = int.Parse(s.Substring(6, 2));
                var hour = int.Parse(s.Substring(8, 2));
                var minute = int.Parse(s.Substring(10, 2));
                var second = int.Parse(s.Substring(12, 2));
                return new DateTime(year, month, day, hour, minute, second);
            }
            catch (System.Exception)
            {
                return null;
            }
        }
    }
}
