using log4net;
using Org.BouncyCastle.Cms;
using Org.BouncyCastle.Tsp;
using Org.BouncyCastle.X509.Store;
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Interop;
using TSManagementApp.Communication;
using TSManagementApp.Enums;
using TSManagementApp.Models;
using TSManagementApp.Properties;
using TSManagementApp.Utils;
using TSManagementApp.Views;

namespace TSManagementApp.ViewModels
{
    public class TimeStampDetailsViewModel : BaseViewModel
    {
        #region Logger
        private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
        #endregion

        private MainWindowViewModel _vmMainWin;
        private MainWindowView _mainWinView;
        public TimeStampDetailsViewModel(MainWindowViewModel vmMainWin, TimeStampReqRes selectedTimeStamp, MainWindowView mainWinView)
        {
            _vmMainWin = vmMainWin;
            _mainWinView = mainWinView;
            SelectedTimeStamp = selectedTimeStamp;

            InitCommands();

            GeneralDetails = new ObservableCollection<Tuple<string, string>>();
            TimeStampDetails = new ObservableCollection<Tuple<string, string>>();
            TimeStampDetailsIsVisible = true;
            ParseResponse();
        }

        #region Properties

        public TimeStampReqRes SelectedTimeStamp { get; set; }
        public ObservableCollection<Tuple<string, string>> GeneralDetails { get; set; }
        public ObservableCollection<Tuple<string, string>> TimeStampDetails { get; set; }

        private bool _timeStampDetailsIsVisible;
        public bool TimeStampDetailsIsVisible { get => _timeStampDetailsIsVisible; set { _timeStampDetailsIsVisible = value; OnPropertyChanged("TimeStampDetailsIsVisible"); } }

        #endregion

        #region Commands, Handlers

        public ICommand CommandShowTsaCert { get; set; }
        public ICommand CommandSaveTimeStampRequest { get; set; }
        public ICommand CommandSaveTimeStamp { get; set; }
        public ICommand CommandSaveTimeStampToken { get; set; }
        

        public void ShowTsaCert()
        {
            try
            {
                TimeStampToken token = null;
                if (SelectedTimeStamp.TimeStampFileType == TimeStampReqRes.FileTypeResponse)
                    token = new TimeStampResponse(SelectedTimeStamp.TimeStampResponse).TimeStampToken;
                else if (SelectedTimeStamp.TimeStampFileType == TimeStampReqRes.FileTypeToken)
                    token = new TimeStampToken(new Org.BouncyCastle.Cms.CmsSignedData(SelectedTimeStamp.TimeStampResponse));
                else
                    throw new Exception(Resources.TsDetailsVM_UnsupportedType);
                if (token != null)
                {
                    IX509Store store = token.GetCertificates("collection");
                    ArrayList certs = new ArrayList(store.GetMatches(null));
                    foreach (Org.BouncyCastle.X509.X509Certificate cert in certs)
                    {
                        if (cert.IssuerDN.ToString(true, new Hashtable()) == token.SignerID.Issuer
                            .ToString(true, new Hashtable()))
                        {
                            X509Certificate2 cert2 = new X509Certificate2(cert.GetEncoded());
                            IntPtr hwnd = new WindowInteropHelper(_mainWinView).Handle;
                            X509Certificate2UI.DisplayCertificate(cert2, hwnd);
                        }
                    }
                }
                else
                    Messages.MsgError(Resources.TsDetailsVM_CantShowTsaCert);
            }
            catch (Exception ex)
            {
                log.Error($"Error while displaying tsa certificate - {ex.ToString()}");
                Messages.MsgError(Resources.TsDetailsVM_CantShowTsaCert);
            }
        }

        public void SaveTimeStampRequest()
        {
            try
            {
                if (SelectedTimeStamp.TimeStampRequest != null)
                {
                    using (SaveFileDialog dialog = new SaveFileDialog())
                    {
                        dialog.AddExtension = false;
                        dialog.DefaultExt = ".tsreq";
                        dialog.Title = Resources.TsDetailsVM_SaveTsreqTitle;
                        dialog.FileName = SelectedTimeStamp.SourceFilePath + ".tsreq";
                        dialog.Filter = $"{Resources.TsDetailsVM_Tsreq}|*.tsreq|{Resources.CreateTsVM_Allfiles}|*.*";
                        var result = dialog.ShowDialog();
                        if(result == DialogResult.OK)
                            File.WriteAllBytes(dialog.FileName, SelectedTimeStamp.TimeStampRequest);
                    }
                }
                else
                    Messages.MsgError(Resources.TsDetailsVM_CantSaveTsreq);
            }
            catch(Exception ex)
            {
                Messages.MsgError($"{Resources.TsDetailsVM_ErrorWhileSavingTsreq} - {ex.Message}");
                log.Error($"Error while saving time stamp request - {ex.ToString()}");
            }
        }

        public void SaveTimeStamp()
        {
            try
            {
                if (SelectedTimeStamp.TimeStampFileType == TimeStampReqRes.FileTypeResponse)
                {
                    using (SaveFileDialog dialog = new SaveFileDialog())
                    {
                        dialog.AddExtension = false;
                        dialog.DefaultExt = ".tsr";
                        dialog.Title = Resources.TsDetailsVM_SaveTsrTitle;
                        dialog.FileName = SelectedTimeStamp.SourceFilePath + ".tsr";
                        dialog.Filter = $"{Resources.TsDetailsVM_TsrFile}|*.tsr|{Resources.CreateTsVM_Allfiles}|*.*";
                        var result = dialog.ShowDialog();
                        if(result == DialogResult.OK)
                            File.WriteAllBytes(dialog.FileName, SelectedTimeStamp.TimeStampResponse);
                    }
                }
                else
                    Messages.MsgError(Resources.TsDetailsVM_CantSaveTsr);
            }
            catch (Exception ex)
            {
                Messages.MsgError($"{Resources.TsDetailsVM_ErrorWhileSavingTsr} - {ex.Message}");
                log.Error($"Error while saving whole time stamp response - {ex.ToString()}");
            }
        }

        public void SaveTimeStampToken()
        {
            try
            {
                using (SaveFileDialog dialog = new SaveFileDialog())
                {
                    dialog.AddExtension = false;
                    dialog.DefaultExt = ".tst";
                    dialog.Title = Resources.TsDetailsVM_SaveTstTitle;
                    dialog.FileName = SelectedTimeStamp.SourceFilePath + ".tst";
                    dialog.Filter = $"{Resources.TsDetailsVM_TstFile}|*.tst|{Resources.CreateTsVM_Allfiles}|*.*";
                    var result = dialog.ShowDialog();
                    if (result == DialogResult.OK)
                    {
                        if (SelectedTimeStamp.TimeStampFileType == TimeStampReqRes.FileTypeResponse)
                            File.WriteAllBytes(dialog.FileName, new TimeStampResponse(SelectedTimeStamp.TimeStampResponse).TimeStampToken.GetEncoded());
                        else if (SelectedTimeStamp.TimeStampFileType == TimeStampReqRes.FileTypeToken)
                            File.WriteAllBytes(dialog.FileName, new TimeStampToken(new Org.BouncyCastle.Cms.CmsSignedData(SelectedTimeStamp.TimeStampResponse)).GetEncoded());
                        else
                            throw new Exception(Resources.TsDetailsVM_UnsupportedTsForm);
                    }
                }
            }
            catch (Exception ex)
            {
                Messages.MsgError($"{Resources.TsDetailsVM_ErrorWhileSavingTst} - {ex.Message}");
                log.Error($"Error while saving time stamp token - {ex.ToString()}");
            }
        }

        #endregion

        #region PrivateMethods

        private void InitCommands()
        {
            CommandShowTsaCert = new CommandGeneric(ShowTsaCert);
            CommandSaveTimeStampRequest = new CommandGeneric(SaveTimeStampRequest);
            CommandSaveTimeStamp = new CommandGeneric(SaveTimeStamp);
            CommandSaveTimeStampToken = new CommandGeneric(SaveTimeStampToken);
        }

        private void ParseResponse()
        {
            try
            {
                if (SelectedTimeStamp != null)
                {
                    log.Info($"Start parsing time stamp to view details - {SelectedTimeStamp.TimeStampFilePath}, type - {SelectedTimeStamp.TimeStampFileType}");
                    if (SelectedTimeStamp.TimeStampFileType == TimeStampReqRes.FileTypeResponse)
                    {
                        TimeStampResponse response = new TimeStampResponse(SelectedTimeStamp.TimeStampResponse);
                        ParseTimeStampResponse(response);
                    }
                    else if (SelectedTimeStamp.TimeStampFileType == TimeStampReqRes.FileTypeToken)
                    {
                        TimeStampToken token = new TimeStampToken(new CmsSignedData(SelectedTimeStamp.TimeStampResponse));
                        ParseTimeStampToken(token);
                    }
                }

            }
            catch(Exception ex)
            {
                log.Error($"Error while parsing response to view details. Exception details - {ex.ToString()}");
                Messages.MsgError($"{Resources.TsDetailsVM_CantViewTsDetails} - {ex.Message}");
                _vmMainWin.GoUcAbout();
            }
        }

        private void ParseTimeStampResponse(TimeStampResponse response)
        {
            try
            {     
                FileInfo tsFileInfo = new FileInfo(SelectedTimeStamp.TimeStampFilePath);

                GeneralDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_TsFilePath, tsFileInfo.FullName));
                GeneralDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_TsSourceFilePath, SelectedTimeStamp.SourceFilePath));

                if (response.TimeStampToken != null)
                {

                    TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_TsaCertIssuer,
                        response.TimeStampToken.SignerID.Issuer.ToString()));
                    TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_GenTime, SelectedTimeStamp.GenTime));
                    TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_OidPolicy, 
                        response.TimeStampToken.TimeStampInfo.Policy));
                    if (response.TimeStampToken.TimeStampInfo.Accuracy?.Millis != null)
                        TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_TimeAccuracy, 
                            $"{response.TimeStampToken.TimeStampInfo.Accuracy.Millis.PositiveValue.ToString()} ms"));
                    else if (response.TimeStampToken.TimeStampInfo.Accuracy?.Micros != null)
                        TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_TimeAccuracy, 
                            $"{response.TimeStampToken.TimeStampInfo.Accuracy.Micros.PositiveValue.ToString()} μs"));
                    else if (response.TimeStampToken.TimeStampInfo.Accuracy?.Seconds != null)
                        TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_TimeAccuracy, 
                            $"{response.TimeStampToken.TimeStampInfo.Accuracy.Seconds.PositiveValue.ToString()} s"));
                    TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_Version, 
                        response.TimeStampToken.TimeStampInfo.TstInfo.Version.ToString()));
                    TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_SerialNumber, 
                        $"{response.TimeStampToken.TimeStampInfo.SerialNumber.ToString()}" +
                        $" ({response.TimeStampToken.TimeStampInfo.SerialNumber.ToString(16).ToUpper()})"));
                    foreach (ShaAlgorithmType sha in Manager.Instance.SettingsManager.TsaShaAlogirthms)
                    {
                        if (sha.Oid == response.TimeStampToken.TimeStampInfo.MessageImprintAlgOid)
                        {
                            TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_ShaAlgorithm, sha.Name));
                            break;
                        }
                    }
                    if (response.TimeStampToken.TimeStampInfo.Nonce != null)
                        TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_Nonce, 
                            $"{response.TimeStampToken.TimeStampInfo.Nonce.ToString()}" +
                            $" ({response.TimeStampToken.TimeStampInfo.Nonce.ToString(16).ToUpper()})"));
                }
                else
                    TimeStampDetailsIsVisible = false;

                string errMessage = VerifyTSResponse(response);
                if (string.IsNullOrWhiteSpace(errMessage))
                    GeneralDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_VerifyResult, Resources.TsDetailsVM_VerifyOk));
                else
                {
                    GeneralDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_VerifyResult, Resources.TsDetailsVM_VerifyError));
                    errMessage = errMessage.Replace("\r\n", " ");
                    GeneralDetails.Add(new Tuple<string, string>("Detail chyby", errMessage));
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        private void ParseTimeStampToken(TimeStampToken token)
        {
            try
            {
                FileInfo tsFileInfo = new FileInfo(SelectedTimeStamp.TimeStampFilePath);

                GeneralDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_TsFilePath, tsFileInfo.FullName));
                GeneralDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_TsSourceFilePath, SelectedTimeStamp.SourceFilePath));

                if (token != null)
                {
                    TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_TsaCertIssuer,
                        token.SignerID.Issuer.ToString()));
                    TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_GenTime, SelectedTimeStamp.GenTime));
                    TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_OidPolicy, token.TimeStampInfo.Policy));
                    if (token.TimeStampInfo.Accuracy.Millis != null)
                        TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_TimeAccuracy, $"{token.TimeStampInfo.Accuracy.Millis.PositiveValue.ToString()} ms"));
                    else if (token.TimeStampInfo.Accuracy.Micros != null)
                        TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_TimeAccuracy, $"{token.TimeStampInfo.Accuracy.Micros.PositiveValue.ToString()} μs"));
                    else if (token.TimeStampInfo.Accuracy.Seconds != null)
                        TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_TimeAccuracy, $"{token.TimeStampInfo.Accuracy.Micros.PositiveValue.ToString()} s"));
                    TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_Version, token.TimeStampInfo.TstInfo.Version.ToString()));
                    TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_SerialNumber, $"{token.TimeStampInfo.SerialNumber.ToString()}" +
                        $" ({token.TimeStampInfo.SerialNumber.ToString(16).ToUpper()})"));
                    foreach (ShaAlgorithmType sha in Manager.Instance.SettingsManager.TsaShaAlogirthms)
                    {
                        if (sha.Oid == token.TimeStampInfo.MessageImprintAlgOid)
                        {
                            TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_ShaAlgorithm, sha.Name));
                            break;
                        }
                    }
                    if (token.TimeStampInfo.Nonce != null)
                        TimeStampDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_Nonce, $"{token.TimeStampInfo.Nonce.ToString()}" +
                            $" ({token.TimeStampInfo.Nonce.ToString(16).ToUpper()})"));
                }
                else
                    TimeStampDetailsIsVisible = false;

                string errMessage = VerifyTSToken(token);
                if (string.IsNullOrWhiteSpace(errMessage))
                    GeneralDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_VerifyResult, Resources.TsDetailsVM_VerifyOk));
                else
                {
                    errMessage = errMessage.Replace("\r\n", " ");
                    GeneralDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_VerifyResult, Resources.TsDetailsVM_VerifyError));
                    GeneralDetails.Add(new Tuple<string, string>(Resources.TsDetailsVM_ErrorDetail, errMessage));
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        private string VerifyTSResponse(TimeStampResponse tsResponse)
        {
            StringBuilder errMsg = new StringBuilder();
            if (tsResponse.Status != (int)TimeStampResponseStatus.Granted && tsResponse.Status != (int)TimeStampResponseStatus.GrantedWithMods)
                errMsg.AppendLine($"{GetEnumDescription.GetTsrPkiFailureInfo(tsResponse.GetFailInfo().IntValue)}");
            try { BouncyCastleTSProvider.ValidateSignature(tsResponse); }
            catch (Exception e) { errMsg.AppendLine(Resources.VerifyTsVM_InvalidSignature); }
            if (tsResponse.TimeStampToken != null)
            {
                string verifyToken = VerifyTSToken(tsResponse.TimeStampToken);
                if(!string.IsNullOrWhiteSpace(verifyToken))
                    errMsg.Append(verifyToken);
            }
            return errMsg.ToString();
        }

        private string VerifyTSToken(TimeStampToken tsToken)
        {
            StringBuilder errMsg = new StringBuilder();
            string hashOid = tsToken.TimeStampInfo.MessageImprintAlgOid;
            System.Security.Cryptography.HashAlgorithm sourceFileImprint = null;
            FileStream sourceFS = new FileStream(SelectedTimeStamp.SourceFilePath, FileMode.Open);
            switch (hashOid)
            {
                case "1.3.14.3.2.26":
                    sourceFileImprint = SHA1.Create();
                    sourceFileImprint.ComputeHash(sourceFS);
                    break;
                case "2.16.840.1.101.3.4.2.1":
                    sourceFileImprint = SHA256.Create();
                    sourceFileImprint.ComputeHash(sourceFS);
                    break;
                case "2.16.840.1.101.3.4.2.3":
                    sourceFileImprint = SHA512.Create();
                    sourceFileImprint.ComputeHash(sourceFS);
                    break;
                default:
                    errMsg.AppendLine(Resources.TsDetailsVM_UnsupportedHash); break;
            }

            if (sourceFileImprint.Hash.Length != tsToken.TimeStampInfo.GetMessageImprintDigest().Length)
                errMsg.AppendLine(Resources.TsDetailsVM_HashLengthNotEqual);
            else if (!CompareTwoHashImprints(sourceFileImprint.Hash, tsToken.TimeStampInfo.GetMessageImprintDigest()))
                errMsg.AppendLine(Resources.TsDetailsVM_MessageImprintNonEqual);

            sourceFS.Dispose();
            if (string.IsNullOrWhiteSpace(errMsg.ToString()))
                return null;
            else
                return errMsg.ToString();
        }

        private bool CompareTwoHashImprints(byte[] hash1, byte[] hash2)
        {
            for (int i = 0; i < hash1.Length; i++)
            {
                if (hash1[i] != hash2[i])
                    return false;
            }
            return true;
        }

        #endregion
    }
}
