using log4net;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Tsp;
using Org.BouncyCastle.X509;
using System;
using System.IO;
using System.Net;
using TSManagementApp.Enums;
using TSManagementApp.Models;
using TSManagementApp.Properties;

namespace TSManagementApp.Communication
{
    /// <summary>
    /// Vytvoří časové razítko s pomocí knihovny BouncyCastle.
    /// </summary>
    public class BouncyCastleTSProvider
    {
        #region Logger
        private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
        #endregion

        #region Properties

        private TspValidationException TokenValidationException { get; set; }
        private TspValidationException SignatureValidationException { get; set; }

        #endregion

        /// <summary>
        /// Vytvoří žádost o časové razítko a poté jí odešle na server a naparsuje odpověď
        /// </summary>
        /// <param name="hashAlgorithm">Hash algoritmus, kterým byl vytvořen otisk podepisovaného souboru</param>
        /// <param name="hashData">Hash otisk souboru</param>
        /// <param name="nonce">Jednoznačný identifikátor. Může být null</param>
        /// <param name="server">Informace o serveru, obsahuje url adresu a přihlašovací údaje</param>
        /// <returns>Naparsovanou odpověď od serveru, která buď obsahuje informace o časovém razítku</returns>
        public TimeStampReqRes RequestTimeStamp(ShaAlgorithmType hashAlgorithm, byte[] hashData, BigInteger nonce, TimeStampServer server,
            string oidPolicy) //TSAProviderResponse
        {
            try
            {
                if (hashData == null)
                    throw new Exception(Resources.TspProvider_HashDataEmpty);

                if (server == null)
                    throw new Exception(Resources.TspProvider_NoUrl);

                if (hashData.Length != HashLength(hashAlgorithm.Name))
                    throw new Exception(Resources.TspProvider_HashLengthError);

                TimeStampRequest timeStampRequest = BuildTimeStampRequest(hashAlgorithm.Oid, hashData, nonce, oidPolicy);
                try
                {
                    HttpWebRequest httpReq = CreateHttpWebRequest(server.Url);
                    WriteRequestToStream(httpReq, timeStampRequest.GetEncoded(), server.Url);
                    TimeStampResponse timeStampResponse = CommunicateWithServer(httpReq);
                    ValidateToken(timeStampRequest, timeStampResponse);
                    if(timeStampResponse.Status == (int)TimeStampResponseStatus.Granted || timeStampResponse.Status == (int)TimeStampResponseStatus.GrantedWithMods)
                        ValidateSignature(timeStampResponse);
                    TimeStampReqRes timeStamp = new TimeStampReqRes();
                    timeStamp.TimeStampRequest = timeStampRequest.GetEncoded();
                    timeStamp.TimeStampResponse = timeStampResponse.GetEncoded();

                    return timeStamp;
                }
                catch (WebException e)
                {
                    log.Error($"Communication with server ends up with exception - {e.ToString()}");
                    throw new Exception($"{Resources.TspProvider_CommWithServerFailed} - {e.Message}");
                }
            }
            catch(Exception ex)
            {
                throw ex;
            }
        }

        private TimeStampRequest BuildTimeStampRequest(string hashAlgorithmOid, byte[] hashData, BigInteger nonce, string oidPolicy)
        {
            TimeStampRequestGenerator reqGen = new TimeStampRequestGenerator();

            /*  RFC 3161: If the certReq field is present and set to true, the TSA's public key
                certificate that is referenced by the ESSCertID identifier inside a
                SigningCertificate attribute in the response MUST be provided by the
                TSA in the certificates field from the SignedData structure in that
                response. */
            reqGen.SetCertReq(true);
            if (oidPolicy != null)
                reqGen.SetReqPolicy(oidPolicy);

            TimeStampRequest request = reqGen.Generate(hashAlgorithmOid, hashData, nonce);

            return request;
        }

        private void WriteRequestToStream(HttpWebRequest httpReq, byte[] reqData, string serverUrl)
        {
            try
            {
                httpReq.ContentLength = reqData.Length;
                Stream reqStream = httpReq.GetRequestStream();
                reqStream.Write(reqData, 0, reqData.Length);
                reqStream.Close();
            }
            catch(Exception ex)
            {
                throw new WebException($"{ex.Message} {Resources.TspProvider_ServerAddress} - {serverUrl}");
            }
        }

        private TimeStampResponse CommunicateWithServer(HttpWebRequest httpReq)
        {
            using (HttpWebResponse httpResp = (HttpWebResponse)httpReq.GetResponse())
            {
                using (Stream respStream = new BufferedStream(httpResp.GetResponseStream()))
                {
                    var response = new TimeStampResponse(respStream);
                    return response;
                }
            }
        }

        private void ValidateToken(TimeStampRequest request, TimeStampResponse response)
        {
            try
            {
                response.Validate(request);
                log.Debug($"Token validated.");
            }
            catch (TspValidationException e)
            {
                log.Error($"TSP token is invalid {e.Message} {response}");
                throw e;
            }
        }

        public static void ValidateSignature(TimeStampResponse response)
        {
            var id = response.TimeStampToken.SignerID;
            var certificates = response.TimeStampToken.GetCertificates("collection");
            var certCollection = certificates.GetMatches(id);
            if (certCollection.Count != 1)
            {
                log.Error($"Cannot find unique certificate in the token. Count is {certCollection.Count} should be 1.");
                throw new Exception(Resources.TspProvider_CertNotFoundInToken);
            }
            var certEnum = certCollection.GetEnumerator();
            certEnum.MoveNext();
            X509Certificate cert = (X509Certificate)certEnum.Current;
            try
            {
                log.Debug($"Validating token signature using X509 subj='{cert.SubjectDN}' sn={cert.SerialNumber}");
                response.TimeStampToken.Validate(cert);
                log.Debug($"Token signature validated.");
            }
            catch (TspValidationException e)
            {
                throw e;
            }
        }

        private int HashLength(string hashAlgorithm)
        {
            switch (hashAlgorithm)
            {
                case "SHA-1": return 160 / 8;
                case "SHA-256": return 256 / 8;
                case "SHA-512": return 512 / 8;
                default:
                    throw new Exception(Resources.TspProvider_UnsupportedHash);
            }
        }

        private HttpWebRequest CreateHttpWebRequest(string serverUrl)
        {
            SettingsModel settings = Manager.Instance.SettingsManager.Settings;

            HttpWebRequest httpReq = (HttpWebRequest)WebRequest.Create(serverUrl);
            httpReq.Method = "POST";
            httpReq.ContentType = "application/timestamp-query";
            // Vložení General settings - vytvoření http reqestu a nastavení typu kominukace dle nastavení
            if (settings.GeneralSettings.IeSettingsIsChecked)
                ;// asi nic nedělat, to nechat tak jak to vytvoří funkce create
            else if (settings.GeneralSettings.ProxyIsChecked)
            {
                CredentialCache credentialCache = new CredentialCache();
                NetworkCredential networkCredential = new NetworkCredential();

                networkCredential.UserName = settings.GeneralSettings.UserName;
                networkCredential.Password = settings.GeneralSettings.Password;
                string[] serverAndPort = settings.GeneralSettings.ProxyServer.Split(':');
                if (serverAndPort.Length != 2)
                    throw new Exception(Resources.TspProvider_ProxyServerBadForm);
                WebProxy proxy = new WebProxy(serverAndPort[0], int.Parse(serverAndPort[1]));
                proxy.Credentials = new NetworkCredential(settings.GeneralSettings.UserName, settings.GeneralSettings.Password);
                proxy.BypassProxyOnLocal = false;
                httpReq.Proxy = proxy;
                httpReq.PreAuthenticate = true;
            }
            else
                throw new Exception(Resources.TspProvider_NoInternetConnMethod);

            //Vložení autentizačních údajů
            if (settings.TsaSettings.AuthLoginDataIsChecked)
                httpReq.Credentials = new NetworkCredential(settings.TsaSettings.LoginName, settings.TsaSettings.LoginPassword);
            else if (settings.TsaSettings.AuthCertIsChecked)
                httpReq.ClientCertificates.Add(settings.TsaSettings.AuthCert);

            return httpReq;
        }
    }
}
