using EUSignCP;
using IWS5.Core.Exceptions;
using IWS5.Core.Models.Configuration;
using IWS5.Core.Models.Request;
using IWS5.Core.Services;
using IWS5.DatabaseAccess.Interfaces;
using IWS5.DatabaseAccess.IwsDataModels;
using IWS5.DatabaseAccess.Repositories;
using Serilog;
using Syncfusion.Drawing;
using Syncfusion.Pdf;
using Syncfusion.Pdf.Graphics;
using Syncfusion.Pdf.Parsing;
using Syncfusion.Pdf.Security;
using System;
using System.Diagnostics;
using System.Runtime.Serialization.Json;
using FluentValidation;
using paiv.uklatn;
using Syncfusion.Pdf.Barcode;
using IWS5.Core.Models;
using IWS5.Core.DTO;

namespace IWS5.Signature
{
    public class EUSignInfo
    {
        public IEUSignCP.EU_CADES_TYPE signType;
        public int signLevel;
        public IEUSignCP.EU_TIME_INFO timeInfo;
        public IEUSignCP.EU_CERT_INFO_EX signerInfo;
        public byte[] signerCert;
    }
    public class SignatureService: ISignatureService
    {
        IDocumentRepository _documentRepository;
        IWorkItemReferenceRepository _workItemReferenceRepository;
        IButtonRepository _buttonRepository;
        ILogger _logger;
        IValidator<SignWorkitemListRequest> _signWorkitemListRequestValidator;

        private CASettings[] CAs = null;
        private IntPtr context = IntPtr.Zero;
        private IntPtr pkContext = IntPtr.Zero;
        private byte[] pkSignCert = null;
        private byte[] pkEnvelopCert = null;

        public SignatureService(
            IDocumentRepository documentRepository, IWorkItemReferenceRepository workItemReference, IButtonRepository buttonRepository, ILogger logger, IValidator<SignWorkitemListRequest> signWorkitemListRequestValidator)
        {
            _documentRepository = documentRepository;
            _workItemReferenceRepository = workItemReference;
            _buttonRepository = buttonRepository;
            _logger = logger;
            _signWorkitemListRequestValidator = signWorkitemListRequestValidator;
        }

        public async Task<CodeResponse<SignatureResponseDTO>> SignWorkItemList(SignWorkitemListRequest signRequest, string userName)
        {
            await _signWorkitemListRequestValidator.ValidateAndThrowAsync(signRequest);
            var jksKeyBytes = signRequest.FileBytes.Select(b => (byte)b).ToArray();
            var signedDocuments = new List<Atdocument>();
            var signedWorkItems = new List<AtworkItemReference>();
            var response = new SignatureResponseDTO();
            foreach (int workitemRefId in signRequest.WorkItemIds)
            {
                var workItem = await _workItemReferenceRepository.FindAsync(
                wReference => wReference.WorkItemReferenceId == workitemRefId,
                wRef => wRef.WorkItem);
                if (!string.IsNullOrEmpty(workItem.WorkItem.LockedBy) &&
                    workItem.WorkItem.LockedBy != userName)
                {
                    _logger.Error("User {UserName} is not allowed to sign documents for WorkItemId {WorkItemId} because it is locked", userName, workItem.WorkItemId);
                    throw new InvalidOperationException($"User {userName} is not allowed to sign documents for WorkItemId {workItem.WorkItemId} because it is locked");
                }
                var documentsList = await _documentRepository.ListAsync(doc => doc.WorkItemId == workItem.WorkItemId);
                if (documentsList.Count == 0)
                {
                    _logger.Error("No documents found for WorkItemId {WorkItemId}", workItem.WorkItemId);
                    response.SkippedWorkItems.Add(workItem.WorkItemReferenceId);
                    continue;
                }
                foreach (var document in documentsList)
                {
                    var signedPdf = SignPdf(document.Document, jksKeyBytes, signRequest.Password, signRequest.issuer);
                    document.Document = signedPdf;
                    document.SignedBy = (document.SignedBy + ',' + userName).TrimStart(',');
                    signedDocuments.Add(document);
                }
                signedWorkItems.Add(workItem);
                response.SignedWorkItems.Add(workItem.WorkItemReferenceId);
            }
            _documentRepository.UpdateRangeAsync(signedDocuments);
            var action = await _buttonRepository.FindAsync(button => button.Queue == signedWorkItems.FirstOrDefault().Queue && button.ButtonIndex == 0);
            string buttonName = action?.ButtonName ?? "SignAction";
            await _workItemReferenceRepository.ForwardWorkItemListAsync(signedWorkItems, buttonName, userName);
            await _workItemReferenceRepository.SaveChangesAsync();
            return new CodeResponse<SignatureResponseDTO>(Core.Enums.ResponseCode.OK, string.Empty, response);
        }

        #region Helper methods

        private void Initialize()
        {
            if (context != IntPtr.Zero)
                return;

            IEUSignCP.SetThrowExceptions(true);
            IEUSignCP.SetUIMode(false);
            IEUSignCP.Initialize();

            IEUSignCP.SetRuntimeParameter(IEUSignCP.EU_SAVE_SETTINGS_PARAMETER, IEUSignCP.EU_SETTINGS_ID_NONE);
            IEUSignCP.SetRuntimeParameter(IEUSignCP.EU_SIGN_TYPE_PARAMETER, IEUSignCP.EU_SIGN_TYPE_CADES_BES);
            IEUSignCP.SetUIMode(false);
            IEUSignCP.SetModeSettings(false);
            IEUSignCP.SetFileStoreSettings("", false, true, false, false, false, false, 3600);
            IEUSignCP.SetProxySettings(
                SignatureValues.PROXY_USE,
                SignatureValues.PROXY_USER != "",
                SignatureValues.PROXY_ADDRESS,
                SignatureValues.PROXY_PORT,
                SignatureValues.PROXY_USER,
                SignatureValues.PROXY_PASSWORD,
                true);
            IEUSignCP.SetOCSPSettings(true, true, SignatureValues.DEFAULT_OCSP_SERVER, "80");
            IEUSignCP.SetOCSPAccessInfoModeSettings(true);

            CAs = LoadCAs();
            foreach (CASettings ca in CAs)
            {
                foreach (string commonName in ca.issuerCNs)
                {
                    IEUSignCP.SetOCSPAccessInfoSettings(
                        commonName, ca.ocspAccessPointAddress,
                        ca.ocspAccessPointPort);
                }
            }

            IEUSignCP.SetTSPSettings(true, SignatureValues.DEFAULT_TSP_SERVER, "80");
            IEUSignCP.SetLDAPSettings(false, "", "", true, "", "");
            IEUSignCP.SetCMPSettings(false, "", "80", "");
            IEUSignCP.SaveCertificates(File.ReadAllBytes(SignatureValues.CA_CERTIFICATES_PATH));
            IEUSignCP.CtxCreate(out context);
        }

        private CASettings[] LoadCAs()
        {
            DataContractJsonSerializer jsonFormatter = new DataContractJsonSerializer(typeof(CASettings[]));
            FileStream fsCAs = new FileStream(
                SignatureValues.CAS_JSON_PATH, FileMode.Open);
            return (CASettings[])jsonFormatter.ReadObject(fsCAs);
        }

        private CASettings GetCA(string caSubjectCN)
        {
            for (int i = 0; i < CAs.Length; i++)
            {
                for (int j = 0; j < CAs[i].issuerCNs.Length; j++)
                {
                    if (caSubjectCN == CAs[i].issuerCNs[j])
                        return CAs[i];
                }
            }

            return null;
        }

        public void LoadKey(byte[] jksBytes, string password, string caIssuerCN)
        {
            Initialize();
            ReadPrivateKey(jksBytes, password, SignatureValues.PK_CERTIFICATES_FILE_NAMES, caIssuerCN);
        }

        private void ReadPrivateKey(byte[] pkFile,
            string password,
            string[] certsFiles, string caIssuerCN)
        {
            List<byte[]> certs = new List<byte[]>();
            IEUSignCP.EU_KEY_MEDIA keyMedia = new IEUSignCP.EU_KEY_MEDIA(0, 0, "");
            CASettings ca = null;
            IEUSignCP.EU_CERT_OWNER_INFO info;
            IEUSignCP.EU_CERT_INFO_EX infoEx;

            string tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".jks");
            File.WriteAllBytes(tempFile, pkFile);

            try
            {
                if (certsFiles != null && certsFiles.Length > 0)
                {
                    for (int i = 0; i < certsFiles.Length; i++)
                    {
                        if (!File.Exists(certsFiles[i]))
                            throw new Exception("Відстуній файл з сертифікатом " + certsFiles[i]);

                        certs.Add(File.ReadAllBytes(certsFiles[i]));
                    }
                }

                string jksAlias = "";

                var psi = new ProcessStartInfo
                {
                    FileName = "keytool",
                    Arguments = $"-list -keystore {tempFile} -storepass {password}",
                    RedirectStandardOutput = true,
                    UseShellExecute = false,
                    CreateNoWindow = true
                };

                using var proc = Process.Start(psi);
                string output = proc.StandardOutput.ReadToEnd();
                proc.WaitForExit();

                foreach (var line in output.Split('\n'))
                {
                    if (line.Contains(",") && !line.StartsWith("Keystore"))
                        jksAlias = line.Split(',')[0].Trim();
                }

                if (jksAlias != null && jksAlias != "")
                {
                    byte[][] certificates;
                    IEUSignCP.GetJKSPrivateKey(pkFile, jksAlias, out pkFile, out certificates);
                    for (int i = 0; i < certificates.Length; i++)
                        certs.Add(certificates[i]);
                }

                if (certs.Count == 0 && caIssuerCN != null && caIssuerCN != "")
                {
                    ca = GetCA(caIssuerCN);
                    if (ca == null)
                        throw new Exception("Налаштування ЦСК не знайдені " + caIssuerCN);

                    if (ca.cmpAddress != "")
                    {
                        byte[] keyInfo;
                        byte[] certsCMP;
                        byte[] cert;

                        IEUSignCP.GetKeyInfoBinary(pkFile, password, out keyInfo);

                        string[] cmpServers = { ca.cmpAddress };
                        string[] cmpServersPorts = { "80" };
                        IEUSignCP.GetCertificatesByKeyInfo(
                            keyInfo, cmpServers, cmpServersPorts, out certsCMP);

                        int i = 0;
                        while (true)
                        {
                            try
                            {
                                IEUSignCP.GetCertificateFromSignedData(
                                    i, certsCMP, out infoEx, out cert);
                                if (infoEx.subjectType == IEUSignCP.EU_SUBJECT_TYPE.END_USER)
                                    certs.Add(cert);
                            }
                            catch (IEUSignCP.EUSignCPException ex)
                            {
                                if (ex.errorCode != IEUSignCP.EU_WARNING_END_OF_ENUM)
                                    throw ex;

                                break;
                            }

                            i++;
                        }
                    }
                }

                for (int i = 0; i < certs.Count; i++)
                    IEUSignCP.SaveCertificate(certs[i]);

                IEUSignCP.CtxReadPrivateKeyBinary(context, pkFile,
                    password, out info, out pkContext);

                try
                {
                    IEUSignCP.CtxGetOwnCertificate(pkContext,
                        IEUSignCP.EU_CERT_KEY_TYPE_DSTU4145,
                        IEUSignCP.EU_KEY_USAGE_DIGITAL_SIGNATURE,
                        out infoEx, out pkSignCert);
                }
                catch (Exception)
                {
                    throw new Exception("Ос. ключ не підтримує підпис за ДСТУ 4145");
                }

                try
                {
                    IEUSignCP.CtxGetOwnCertificate(pkContext,
                        IEUSignCP.EU_CERT_KEY_TYPE_DSTU4145,
                        IEUSignCP.EU_KEY_USAGE_KEY_AGREEMENT,
                        out infoEx, out pkEnvelopCert);
                }
                catch (Exception)
                {
                    throw new Exception("Ос. ключ не підтримує напралене шифрування даних за ДСТУ 4145");
                }
            }
            catch (Exception e)
            {
                if (pkContext != IntPtr.Zero)
                {
                    IEUSignCP.CtxFreePrivateKey(pkContext);
                    pkContext = IntPtr.Zero;
                }

                throw e;
            }
            finally
            {
                File.Delete(tempFile);
            }
        }

        private byte[] SignData(byte[] data, bool detached)
        {
            if (pkContext == IntPtr.Zero)
                throw new SignatureException("Private key is not loaded.");

            const int signAlgo = IEUSignCP.EU_CTX_SIGN_ALGO_DSTU4145_WITH_GOST34311;
            const bool appendCert = true;

            byte[] signed;
            int error = IEUSignCP.CtxSignData(pkContext, signAlgo, data, detached, appendCert, out signed);
            if (error != IEUSignCP.EU_ERROR_NONE)
                throw new SignatureException($"CtxSignData failed (code {error}).");
            return signed;
        }

        private IEUSignCP.EU_CADES_TYPE GetSignDataCAdESType(byte[] signedData)
        {
            bool isDataAvail;
            int err = IEUSignCP.CtxIsDataInSignedDataAvailable(
                context,
                signedData,
                out isDataAvail
            );
            if (err != IEUSignCP.EU_ERROR_NONE)
                throw new SignatureException($"CtxIsDataInSignedDataAvailable error: {err}");
            return isDataAvail
                ? IEUSignCP.EU_CADES_TYPE.ENVELOPED
                : IEUSignCP.EU_CADES_TYPE.DETACHED;
        }

        private void VerifyData(
            IEUSignCP.EU_CADES_TYPE signType,
            byte[] signedData,
            byte[] data,
            List<EUSignInfo> signInfos)
        {
            int count;
            int err = IEUSignCP.CtxGetSignsCount(context, signedData, out count);
            if (err != IEUSignCP.EU_ERROR_NONE)
                throw new SignatureException($"CtxGetSignsCount error: {err}");

            for (int i = 0; i < count; i++)
            {
                var info = new EUSignInfo
                {
                    signType = signType,
                    signerCert = new byte[0]
                };

                IEUSignCP.EU_SIGN_INFO nativeInfo;
                err = IEUSignCP.VerifyDataSpecific(
                    data,
                    i,
                    signedData,
                    out nativeInfo
                );
                if (err != IEUSignCP.EU_ERROR_NONE)
                    throw new SignatureException($"VerifyDataSpecific error #{i}: {err}");

                err = IEUSignCP.GetSignType(i, signedData, out info.signLevel);
                if (err != IEUSignCP.EU_ERROR_NONE)
                    throw new SignatureException($"GetSignType error #{i}: {err}");

                err = IEUSignCP.GetSignTimeInfo(i, signedData, out info.timeInfo);
                if (err != IEUSignCP.EU_ERROR_NONE)
                    throw new SignatureException($"GetSignTimeInfo error #{i}: {err}");

                err = IEUSignCP.GetSignerInfo(i, signedData, out info.signerInfo, ref info.signerCert);
                if (err != IEUSignCP.EU_ERROR_NONE)
                    throw new SignatureException($"GetSignerInfo error #{i}: {err}");

                signInfos.Add(info);
            }
        }

        private List<EUSignInfo> VerifyData(
            byte[] signedData,
            byte[] originalData = null)
        {
            var result = new List<EUSignInfo>();
            byte[] data = originalData;

            try
            {
                var type = GetSignDataCAdESType(signedData);

                VerifyData(type, signedData, data, result);

                while (type == IEUSignCP.EU_CADES_TYPE.ENVELOPED
                       && IEUSignCP.IsSignedData(data)
                       && GetSignDataCAdESType(data) == IEUSignCP.EU_CADES_TYPE.ENVELOPED)
                {

                    byte[] extracted;
                    int err = IEUSignCP.CtxGetDataFromSignedData(
                        context,
                        data,
                        out extracted
                    );
                    if (err != IEUSignCP.EU_ERROR_NONE)
                        throw new SignatureException($"CtxGetDataFromSignedData error: {err}");

                    data = extracted;
                    VerifyData(IEUSignCP.EU_CADES_TYPE.ENVELOPED, signedData, data, result);
                }
            }
            catch
            {
                throw;
            }

            return result;
        }

        private string DateToString(IEUSignCP.SYSTEMTIME time)
        {
            DateTime date = new DateTime((int)time.wYear,
                (int)time.wMonth, (int)time.wDay, (int)time.wHour,
                (int)time.wMinute, (int)time.wSecond);
            return date.ToString("dd.MM.yyyy HH:mm:ss");
        }

        private byte[] SignPdf(byte[] pdfBytes,
                            byte[] jksBytes,
                            string jksPassword,
                            string caIssuerCN)
        {
            LoadKey(jksBytes, jksPassword, caIssuerCN);
            var cadesSignature = SignData(pdfBytes, true);
            var infos = VerifyData(cadesSignature, pdfBytes);

            using var loadedDoc = new PdfLoadedDocument(new MemoryStream(pdfBytes));
            var lastPage = (PdfLoadedPage)loadedDoc.Pages[loadedDoc.Pages.Count - 1];

            int signatureIndex =
                CountSignaturesOnPage(loadedDoc, lastPage);

            var signature = new PdfSignature(
                loadedDoc,
                lastPage,
                (PdfCertificate)null,
                $"SignatureField{signatureIndex + 1}")
            {
                Bounds = new RectangleF(20, 20, 100, 100),
                Settings =
        {
            CryptographicStandard = CryptographicStandard.CADES,
            DigestAlgorithm = DigestAlgorithm.SHA256
        },
                Reason = "Підписано через EUSignCP",
                ContactInfo = "andriy@company.com"
            };
            signature.ComputeHash += (sender, args) =>
            {
                args.SignedData = cadesSignature;
            };

            var graphics = lastPage.Graphics;
            string fontPath = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.Fonts),
                "times.ttf");
            var font = new PdfTrueTypeFont(fontPath, 10);
            var brush = new PdfSolidBrush(Color.Black);
            float offsetY = 20;
            float offsetX = 20;

            PdfQRBarcode barcode = new PdfQRBarcode();
            barcode.XDimension = 3;
            barcode.ErrorCorrectionLevel = PdfErrorCorrectionLevel.Medium;
            PdfBarcodeQuietZones quietZones = new PdfBarcodeQuietZones();
            quietZones.All = 4f;
            barcode.QuietZone = quietZones;
            barcode.Size = new SizeF(50, 50);


            foreach (var info in infos)
            {
                if (signatureIndex > 0)
                {
                    var positionInRow = signatureIndex % 2;
                    offsetX = 300 * positionInRow;
                    if (positionInRow == 0) // every next signature after 2rd move to new row
                    {
                        offsetX = 20;
                    }
                    if (signatureIndex >= 2 && signatureIndex <= 3)
                    {
                        offsetY += 50;
                    }
                    else if (signatureIndex >= 4 && signatureIndex <= 5)
                    {
                        offsetY += 100;
                    }
                }

                if (info.timeInfo.timeAvail)
                {
                    graphics.DrawString(((info.timeInfo.timeStamp) ? "Time stamp(data):" : "Sign time:") + " " +
                        DateToString(info.timeInfo.time), font, brush, new PointF(offsetX, lastPage.Size.Height - offsetY));
                }

                if (info.timeInfo.signTimeStampAvail)
                {
                    graphics.DrawString($"Time stamp(signature): {DateToString(info.timeInfo.signTimeStamp)}", font, brush, new PointF(offsetX, lastPage.Size.Height - offsetY));
                }

                if (!info.timeInfo.timeAvail && !info.timeInfo.signTimeStampAvail)
                    graphics.DrawString($"Sign time: not available", font, brush, new PointF(offsetX, lastPage.Size.Height - offsetY));
                offsetY += 15;

                graphics.DrawString(
                    $"Issuer: {info.signerInfo.issuerCN}", font, brush,
                    new PointF(offsetX, lastPage.Size.Height - offsetY));
                offsetY += 15;

                graphics.DrawString(
                    $"Subject: {info.signerInfo.subjCN}", font, brush,
                    new PointF(offsetX, lastPage.Size.Height - offsetY));
                offsetY += 20;

                UkrainianLatin tr = new UkrainianLatin();
                var issuerEn = tr.Encode(info.signerInfo.issuerCN, UkrainianLatin.Table.DSTU_9112_B);
                var signerEn = tr.Encode(info.signerInfo.subjCN, UkrainianLatin.Table.DSTU_9112_B);

                barcode.Text += $"Issuer:{issuerEn}; Subject:{signerEn}; Expired:{DateToString(info.signerInfo.certEndTime)}";
                barcode.Draw(lastPage, new PointF(offsetX + 210, lastPage.Size.Height - offsetY + 20));
            }

            using var outStream = new MemoryStream();
            loadedDoc.Save(outStream);
            return outStream.ToArray();
        }

        int CountSignaturesOnPage(PdfLoadedDocument doc, PdfLoadedPage page)
        {
            if (doc?.Form == null) return 0;

            return doc.Form.Fields
                .OfType<PdfLoadedSignatureField>()
                .Count(f => f.Page == page);
        }
        #endregion

    }
}
