/*++

Program name:

  dm

Module Name:

  WebService.cpp

Notices:

  WebService - Deal Module Web Service

Author:

  Copyright (c) Prepodobny Alen

  mailto: alienufo@inbox.ru
  mailto: ufocomp@gmail.com

--*/

//----------------------------------------------------------------------------------------------------------------------

#include "Core.hpp"
#include "WebService.hpp"
//----------------------------------------------------------------------------------------------------------------------

#define BPS_PGP_HASH "SHA512"
#define BM_PREFIX "BM-"
//----------------------------------------------------------------------------------------------------------------------

#define SYSTEM_PROVIDER_NAME "system"
#define SERVICE_APPLICATION_NAME "service"
#define CONFIG_SECTION_NAME "module"
//----------------------------------------------------------------------------------------------------------------------

extern "C++" {

namespace Apostol {

    namespace Module {

        //--------------------------------------------------------------------------------------------------------------

        //-- CWebService -----------------------------------------------------------------------------------------------

        //--------------------------------------------------------------------------------------------------------------

        CWebService::CWebService(CModuleProcess *AProcess, const CString& ModuleName, const CString& SectionName):
                CCustomModule(AProcess, ModuleName, SectionName) {
            
            m_NexServerTime = 0;
            CWebService::InitMethods();

            InitServerList();
        }
        //--------------------------------------------------------------------------------------------------------------

        void CWebService::InitMethods() {
            CCustomModule::InitMethods();
        }
        //--------------------------------------------------------------------------------------------------------------

        void CWebService::InitServerList() {
            m_DefaultServer.Value().PGP().Add(CKeyContext(CString("PUBLIC"), CString()));
            m_DefaultServer.Value().PGP().Add(CKeyContext(m_DefaultServer.Name(), CString()));

            if (m_Servers.Count() == 0) {
#ifdef _DEBUG
                const int index = m_Servers.AddPair(BPS_DEBUG_SERVER_URL, CContext(BPS_BM_DEBUG_ADDRESS, CLocation(BPS_DEBUG_SERVER_URL)));
                auto &Keys = m_Servers[index].Value().PGP();
                Keys.Add(CKeyContext(CString("PUBLIC"), CString()));
                Keys.Add(CKeyContext(CString(BPS_BM_DEBUG_ADDRESS), CString()));
#endif
                m_Servers.Add(m_DefaultServer);
            }
        }
        //--------------------------------------------------------------------------------------------------------------

        int CWebService::CurrentContextIndex(const CString &Params) {
            int index = 0;
            while (index < m_Servers.Count()) {
                const auto &caContext = m_Servers[index].Value();
                if (caContext.Status() == Context::csRunning) {
                    if (Params.IsEmpty() || Params == caContext.Name() || Params == caContext.URL().Origin()) {
                        return index;
                    }
                }

                index++;
            }

            return -1;
        }
        //--------------------------------------------------------------------------------------------------------------

        bool CWebService::DoProxyExecute(CTCPConnection *AConnection) {

            const auto pConnection = dynamic_cast<CHTTPClientConnection *> (AConnection);
            const auto pProxy = dynamic_cast<CHTTPProxy *> (pConnection->Client());

            if (Assigned(pProxy) && pProxy->Connection() != nullptr) {
                const auto pProxyConnection = pProxy->Connection();
                const auto &caServerRequest = pProxyConnection->Request();
                auto &ServerReply = pProxyConnection->Reply();
                const auto &caProxyReply = pConnection->Reply();

                DebugRequest(pConnection->Request());
                DebugReply(caProxyReply);

                const auto &caFormat = caServerRequest.Params["payload"];
                if (!caFormat.IsEmpty()) {
                    if (caFormat == "html") {
                        ServerReply.ContentType = CHTTPReply::html;
                    } else if (caFormat == "json") {
                        ServerReply.ContentType = CHTTPReply::json;
                    } else if (caFormat == "xml") {
                        ServerReply.ContentType = CHTTPReply::xml;
                    } else {
                        ServerReply.ContentType = CHTTPReply::text;
                    }

                    if (caProxyReply.Status == CHTTPReply::ok) {
                        if (!caProxyReply.Content.IsEmpty()) {
                            const CJSON json(caProxyReply.Content);
                            ServerReply.Content = base64_decode(json["payload"].AsString());
                        }
                        pProxyConnection->SendReply(caProxyReply.Status, nullptr, true);
                    } else {
                        if (caProxyReply.Status == CHTTPReply::forbidden) {
                            const auto &caServerParam = caServerRequest.Params["server"];
                            const auto index = CurrentContextIndex(caServerParam);

                            auto &Context = index == -1 ? m_CurrentServer : m_Servers[index].Value();

                            Context.SetCheckDate(0);
                            Context.SetStatus(Context::csInitialized);
                        }

                        ServerReply.Content = caProxyReply.Content;
                        pProxyConnection->SendStockReply(caProxyReply.Status, true);
                    }
                } else {
                    ServerReply.Content = caProxyReply.Content;

                    if (caProxyReply.Status == CHTTPReply::ok) {
                        pProxyConnection->SendReply(caProxyReply.Status, nullptr, true);
                    } else {
                        if (caProxyReply.Status == CHTTPReply::forbidden) {
                            const auto &caServerParam = caServerRequest.Params["server"];
                            const auto index = CurrentContextIndex(caServerParam);

                            auto &Context = index == -1 ? m_CurrentServer : m_Servers[index].Value();

                            Context.SetCheckDate(0);
                            Context.SetStatus(Context::csInitialized);
                        }

                        pProxyConnection->SendStockReply(caProxyReply.Status, true);
                    }
                }
            }

            pConnection->CloseConnection(true);

            return true;
        }
        //--------------------------------------------------------------------------------------------------------------

        void CWebService::DoProxyException(CTCPConnection *AConnection, const Delphi::Exception::Exception &E) {
            const auto pConnection = dynamic_cast<CHTTPClientConnection*> (AConnection);
            const auto pProxy = dynamic_cast<CHTTPProxy*> (pConnection->Client());

            if (Assigned(pProxy) && (pProxy->Connection() != nullptr)) {
                auto pProxyConnection = pProxy->Connection();
                const auto &caServerRequest = pProxyConnection->Request();
                auto &ServerReply = pProxyConnection->Reply();

                const auto &Format = caServerRequest.Params["format"];
                if (Format == "html") {
                    ServerReply.ContentType = CHTTPReply::html;
                }

                try {
                    pProxyConnection->SendStockReply(CHTTPReply::bad_gateway, true);
                } catch (...) {

                }
            }

            Log()->Error(APP_LOG_EMERG, 0, "[%s:%d] %s", pProxy->Host().c_str(), pProxy->Port(), E.what());
        }
        //--------------------------------------------------------------------------------------------------------------

        void CWebService::DoEventHandlerException(CPollEventHandler *AHandler, const Delphi::Exception::Exception &E) {
            auto pConnection = dynamic_cast<CHTTPClientConnection*> (AHandler->Binding());
            auto pProxy = dynamic_cast<CHTTPProxy*> (pConnection->Client());

            if (Assigned(pProxy) && (pProxy->Connection() != nullptr)) {
                auto pProxyConnection = pProxy->Connection();
                auto &Reply = pProxyConnection->Reply();
                ExceptionToJson(0, E, Reply.Content);
                pProxyConnection->SendReply(CHTTPReply::internal_server_error, nullptr, true);
            }

            Log()->Error(APP_LOG_EMERG, 0, E.what());
        }
        //--------------------------------------------------------------------------------------------------------------

        void CWebService::DoProxyConnected(CObject *Sender) {
            auto pConnection = dynamic_cast<CHTTPClientConnection*> (Sender);
            if (pConnection != nullptr) {
                Log()->Message(_T("[%s:%d] Client connected."), pConnection->Socket()->Binding()->PeerIP(),
                               pConnection->Socket()->Binding()->PeerPort());
            }
        }
        //--------------------------------------------------------------------------------------------------------------

        void CWebService::DoProxyDisconnected(CObject *Sender) {
            const auto pConnection = dynamic_cast<CHTTPClientConnection*> (Sender);
            if (pConnection != nullptr) {
                Log()->Message(_T("[%s:%d] Client disconnected."), pConnection->Socket()->Binding()->PeerIP(),
                               pConnection->Socket()->Binding()->PeerPort());
            }
        }
        //--------------------------------------------------------------------------------------------------------------

        CHTTPProxy *CWebService::GetProxy(CHTTPServerConnection *AConnection) {
            auto pProxy = m_ProxyManager.Add(AConnection);
#if defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE >= 9)
            //pProxy->OnVerbose([this](auto && Sender, auto && AFormat, auto && args) { DoVerbose(Sender, AFormat, args); });

            pProxy->OnExecute([this](auto && AConnection) { return DoProxyExecute(AConnection); });

            pProxy->OnException([this](auto && AConnection, auto && E) { DoProxyException(AConnection, E); });
            pProxy->OnEventHandlerException([this](auto && AHandler, auto && E) { DoEventHandlerException(AHandler, E); });

            pProxy->OnConnected([this](auto && Sender) { DoProxyConnected(Sender); });
            pProxy->OnDisconnected([this](auto && Sender) { DoProxyDisconnected(Sender); });
#else
            //pProxy->OnVerbose(std::bind(&CWebService::DoVerbose, this, _1, _2, _3));

            pProxy->OnExecute(std::bind(&CWebService::DoProxyExecute, this, _1));

            pProxy->OnException(std::bind(&CWebService::DoProxyException, this, _1, _2));
            pProxy->OnEventHandlerException(std::bind(&CWebService::DoEventHandlerException, this, _1, _2));

            pProxy->OnConnected(std::bind(&CWebService::DoProxyConnected, this, _1));
            pProxy->OnDisconnected(std::bind(&CWebService::DoProxyDisconnected, this, _1));
#endif
            return pProxy;
        }
        //--------------------------------------------------------------------------------------------------------------

        void CWebService::DoAccount(CHTTPServerConnection *AConnection, const CString &Method, const CString &URI) {

            auto pProxy = GetProxy(AConnection);
            const auto &caServerRequest = AConnection->Request();
            auto &ProxyRequest = pProxy->Request();

            const auto &caModuleAddress = m_Module["address"];
            const auto &caHost = caServerRequest.Headers["host"];
            const auto &caOrigin = caServerRequest.Headers["origin"];

            const auto &caUserAddress = GetParam(caServerRequest, "address");
            const auto &pgpValue = GetParam(caServerRequest, "pgp");
            const auto &caServerParam = GetParam(caServerRequest, "server");

            const auto index = CurrentContextIndex(caServerParam);

            const auto &caContext = index == -1 ? m_CurrentServer : m_Servers[index].Value();
            const auto &caServer = caContext.URL().Origin();

            CString sPayload;

            if (!caServerRequest.Content.IsEmpty()) {
                CStringList caClearText;

                const auto &ContentType = caServerRequest.Headers.Values(_T("content-type"));

                if (ContentType.Find("application/x-www-form-urlencoded") == 0) {

                    const CStringList &FormData = caServerRequest.FormData;

                    const auto &formDate = FormData["date"];
                    const auto &formAddress = FormData["address"];
                    const auto &formBitmessage = FormData["bitmessage"];
                    const auto &formKey = FormData["key"];
                    const auto &formPGP = FormData["pgp"];
                    const auto &formURL = FormData["url"];
                    const auto &formFlags = FormData["flags"];
                    const auto &formSign = FormData["sign"];

                    if (!formDate.IsEmpty()) {
                        caClearText << formDate;
                    }

                    if (!formAddress.IsEmpty()) {
                        caClearText << formAddress;
                    }

                    if (!formFlags.IsEmpty()) {
                        caClearText << formFlags;
                    }

                    if (!formBitmessage.IsEmpty()) {
                        caClearText << formBitmessage;
                    }

                    if (!formKey.IsEmpty()) {
                        caClearText << formKey;
                    }

                    if (!formPGP.IsEmpty()) {
                        caClearText << formPGP;
                    }

                    if (!formURL.IsEmpty()) {
                        caClearText << formURL;
                    }

                    if (!formSign.IsEmpty()) {
                        caClearText << formSign;
                    }

                } else if (ContentType.Find("multipart/form-data") == 0) {

                    CFormData FormData;
                    CHTTPRequestParser::ParseFormData(caServerRequest, FormData);

                    const auto &formDate = FormData.Data("date");
                    const auto &formAddress = FormData.Data("address");
                    const auto &formBitmessage = FormData.Data("bitmessage");
                    const auto &formKey = FormData.Data("key");
                    const auto &formPGP = FormData.Data("pgp");
                    const auto &formURL = FormData.Data("url");
                    const auto &formFlags = FormData.Data("flags");
                    const auto &formSign = FormData.Data("sign");

                    if (!formDate.IsEmpty()) {
                        caClearText << formDate;
                    }

                    if (!formAddress.IsEmpty()) {
                        caClearText << formAddress;
                    }

                    if (!formFlags.IsEmpty()) {
                        caClearText << formFlags;
                    }

                    if (!formBitmessage.IsEmpty()) {
                        caClearText << formBitmessage;
                    }

                    if (!formKey.IsEmpty()) {
                        caClearText << formKey;
                    }

                    if (!formPGP.IsEmpty()) {
                        caClearText << formPGP;
                    }

                    if (!formURL.IsEmpty()) {
                        caClearText << formURL;
                    }

                    if (!formSign.IsEmpty()) {
                        caClearText << formSign;
                    }

                } else if (ContentType.Find("application/json") == 0) {

                    const CJSON contextJson(caServerRequest.Content);

                    const auto &jsonDate = contextJson["date"].AsString();
                    const auto &jsonAddress = contextJson["address"].AsString();
                    const auto &jsonBitmessage = contextJson["bitmessage"].AsString();
                    const auto &jsonKey = contextJson["key"].AsString();
                    const auto &jsonPGP = contextJson["pgp"].AsString();
                    const auto &jsonFlags = contextJson["flags"].AsString();
                    const auto &jsonSign = contextJson["sign"].AsString();

                    if (!jsonDate.IsEmpty()) {
                        caClearText << jsonDate;
                    }

                    if (!jsonAddress.IsEmpty()) {
                        caClearText << jsonAddress;
                    }

                    if (!jsonFlags.IsEmpty()) {
                        caClearText << jsonFlags;
                    }

                    if (!jsonBitmessage.IsEmpty()) {
                        caClearText << jsonBitmessage;
                    }

                    if (!jsonKey.IsEmpty()) {
                        caClearText << jsonKey;
                    }

                    if (!jsonPGP.IsEmpty()) {
                        caClearText << jsonPGP;
                    }

                    const CJSONValue &jsonURL = contextJson["url"];
                    if (jsonURL.IsArray()) {
                        const CJSONArray &arrayURL = jsonURL.Array();
                        for (int i = 0; i < arrayURL.Count(); i++) {
                            caClearText << arrayURL[i].AsString();
                        }
                    }

                    if (!jsonSign.IsEmpty()) {
                        caClearText << jsonSign;
                    }

                } else {
                    caClearText = caServerRequest.Content;
                }

                if (pgpValue == "off" || pgpValue == "false") {
                    sPayload = caClearText.Text();
                } else {
                    Apostol::PGP::CleartextSignature(
                            m_pgpModuleKey,
                            m_pgpPassphrase,
                            BPS_PGP_HASH,
                            caClearText.Text(),
                            sPayload);
                }
            }

            CJSON Json(jvtObject);
            CJSONValue Module(jvtObject);

            Module.Object().AddPair("address", caModuleAddress);

            Json.Object().AddPair("id", GetUID(16).Lower());
            Json.Object().AddPair("host", caHost);

            if (!caOrigin.IsEmpty()) {
                Json.Object().AddPair("origin", caOrigin);
            } else {
                Json.Object().AddPair("origin", GetProtocol(AConnection) + "://" + caHost);
            }

            Json.Object().AddPair("module", Module);
            Json.Object().AddPair("address", caUserAddress.IsEmpty() ? caModuleAddress : caUserAddress);

            if (!sPayload.IsEmpty())
                Json.Object().AddPair("payload", base64_encode(sPayload));

            CLocation Location(caServer);

            pProxy->Host() = Location.hostname;
            pProxy->Port(Location.port == 0 ? BPS_SERVER_PORT : Location.port);

            ProxyRequest.Clear();

            ProxyRequest.Location = Location.hostname + caServerRequest.Location.pathname + caServerRequest.Location.search;
            ProxyRequest.CloseConnection = false;
            ProxyRequest.ContentType = CHTTPRequest::json;
            ProxyRequest.Content << Json;

            CHTTPRequest::Prepare(ProxyRequest, "POST", URI.c_str());

            ProxyRequest.Headers.Values("Host", ProxyRequest.Location.Host());

            ProxyRequest.AddHeader("Authorization", "Bearer " + caContext.Tokens()["access_token"]);

            if (!caModuleAddress.IsEmpty())
                ProxyRequest.AddHeader("Module-Address", caModuleAddress);

            if (!caOrigin.IsEmpty())
                ProxyRequest.AddHeader("Origin", caOrigin);

            AConnection->CloseConnection(false);

            pProxy->AutoFree(true);
            pProxy->UsedSSL(Location.protocol == HTTPS_PREFIX);
            pProxy->Active(true);
        }
        //--------------------------------------------------------------------------------------------------------------

        void CWebService::DoDeal(CHTTPServerConnection *AConnection, const CString &Method, const CString &URI, const CString &Action) {

            auto pProxy = GetProxy(AConnection);
            const auto &caServerRequest = AConnection->Request();
            auto &ProxyRequest = pProxy->Request();

            const auto &caModuleAddress = m_Module["address"];
            const auto &caModuleFee = m_Module["fee"];

            const auto checkFee = CheckFee(caModuleFee);
            if (checkFee == -1)
                throw ExceptionFrm("Invalid module fee value: %s", caModuleFee.c_str());

            const auto &caHost = caServerRequest.Headers["host"];
            const auto &caOrigin = caServerRequest.Headers["origin"];

            const auto &code = GetParam(caServerRequest, "code");
            const auto &address = GetParam(caServerRequest, "address");
            const auto &pgpValue = GetParam(caServerRequest, "pgp");
            const auto &caServerParam = GetParam(caServerRequest, "server");

            const auto &caUserAddress = address.length() == 40 ? CString() : address;
            const auto &caDealCode = !code.empty() ? code : address.length() == 40 ? address : CString();

            const auto index = CurrentContextIndex(caServerParam);

            const auto &caContext = index == -1 ? m_CurrentServer : m_Servers[index].Value();
            const auto &caServer = caContext.URL().Origin();

            YAML::Node Node;

            CString sPayload;

            if (!caServerRequest.Content.IsEmpty() && Action != "status") {

                const auto &ContentType = caServerRequest.Headers.Values(_T("content-type"));

                if (ContentType.Find("application/x-www-form-urlencoded") == 0) {

                    const CStringList &FormData = caServerRequest.FormData;

                    const auto &formType = FormData["type"];
                    const auto &formAt = FormData["at"];
                    const auto &formDate = FormData["date"];
                    const auto &formSalt = FormData["salt"];
                    const auto &formSellerAddress = FormData["seller_address"];
                    const auto &formSellerRating = FormData["seller_rating"];
                    const auto &formSellerSignature = FormData["seller_signature"];
                    const auto &formCustomerAddress = FormData["customer_address"];
                    const auto &formCustomerRating = FormData["customer_rating"];
                    const auto &formCustomerSignature = FormData["customer_signature"];
                    const auto &formPaymentAddress = FormData["payment_address"];
                    const auto &formPaymentUntil = FormData["payment_until"];
                    const auto &formPaymentSum = FormData["payment_sum"];
                    const auto &formFeedbackLeaveBefore = FormData["feedback_leave_before"];
                    const auto &formFeedbackStatus = FormData["feedback_status"];
                    const auto &formFeedbackComments = FormData["feedback_comments"];
                    const auto &formFeedbackRefund = FormData["feedback_refund"];

                    CheckKeyForNull("order", Action.c_str());
                    CheckKeyForNull("type", formType.c_str());
                    CheckKeyForNull("at", formAt.c_str());
                    CheckKeyForNull("seller_address", formSellerAddress.c_str());
                    CheckKeyForNull("customer_address", formCustomerAddress.c_str());
                    CheckKeyForNull("payment_sum", formPaymentSum.c_str());

                    if (Action == "cancel") {
                        CheckKeyForNull("seller_signature", formSellerSignature.c_str());
                    }

                    if (Action == "feedback") {
                        CheckKeyForNull("customer_signature", formCustomerSignature.c_str());
                    }

                    YAML::Node Deal = Node["deal"];

                    Deal["order"] = Action.c_str();
                    Deal["type"] = formType.c_str();

                    Deal["at"] = formAt.c_str();
                    Deal["date"] = formDate.IsEmpty() ? UTCFormat(DateToString(UTC())).c_str() : formDate.c_str();
                    Deal["salt"] = formSalt.IsEmpty() ? GenSalt().c_str() : formSalt.c_str();

                    YAML::Node Seller = Deal["seller"];

                    Seller["address"] = formSellerAddress.c_str();

                    if (!formSellerRating.IsEmpty())
                        Seller["rating"] = formSellerRating.c_str();

                    if (!formSellerSignature.IsEmpty())
                        Seller["signature"] = formSellerSignature.c_str();

                    YAML::Node Customer = Deal["customer"];

                    Customer["address"] = formCustomerAddress.c_str();

                    if (!formCustomerRating.IsEmpty())
                        Customer["rating"] = formCustomerRating.c_str();

                    if (!formCustomerSignature.IsEmpty())
                        Customer["signature"] = formCustomerSignature.c_str();

                    YAML::Node Payment = Deal["payment"];

                    if (!formPaymentAddress.IsEmpty())
                        Payment["address"] = formPaymentAddress.c_str();

                    if (!formPaymentUntil.IsEmpty())
                        Payment["until"] = formPaymentUntil.c_str();

                    Payment["sum"] = formPaymentSum.c_str();

                    if (!formFeedbackLeaveBefore.IsEmpty()) {
                        YAML::Node Feedback = Deal["feedback"];

                        Feedback["leave-before"] = formFeedbackLeaveBefore.c_str();

                        if (!formFeedbackStatus.IsEmpty())
                            Feedback["status"] = formFeedbackStatus.c_str();

                        if (!formFeedbackComments.IsEmpty())
                            Feedback["comments"] = formFeedbackComments.c_str();

                        if (!formFeedbackRefund.IsEmpty())
                            Feedback["refund"] = formFeedbackRefund.c_str();
                    }

                } else if (ContentType.Find("multipart/form-data") == 0) {

                    CFormData FormData;
                    CHTTPRequestParser::ParseFormData(caServerRequest, FormData);

                    const auto &formType = FormData.Data("type");
                    const auto &formAt = FormData.Data("at");
                    const auto &formDate = FormData.Data("date");
                    const auto &formSalt = FormData.Data("salt");
                    const auto &formSellerAddress = FormData.Data("seller_address");
                    const auto &formSellerRating = FormData.Data("seller_rating");
                    const auto &formSellerSignature = FormData.Data("seller_signature");
                    const auto &formCustomerAddress = FormData.Data("customer_address");
                    const auto &formCustomerRating = FormData.Data("customer_rating");
                    const auto &formCustomerSignature = FormData.Data("customer_signature");
                    const auto &formPaymentAddress = FormData.Data("payment_address");
                    const auto &formPaymentUntil = FormData.Data("payment_until");
                    const auto &formPaymentSum = FormData.Data("payment_sum");
                    const auto &formFeedbackLeaveBefore = FormData.Data("feedback_leave_before");
                    const auto &formFeedbackStatus = FormData.Data("feedback_status");
                    const auto &formFeedbackComments = FormData.Data("feedback_comments");
                    const auto &formFeedbackRefund = FormData.Data("feedback_refund");

                    CheckKeyForNull("order", Action.c_str());
                    CheckKeyForNull("type", formType.c_str());
                    CheckKeyForNull("at", formAt.c_str());
                    CheckKeyForNull("seller_address", formSellerAddress.c_str());
                    CheckKeyForNull("customer_address", formCustomerAddress.c_str());
                    CheckKeyForNull("payment_sum", formPaymentSum.c_str());

                    if (Action == "cancel") {
                        CheckKeyForNull("seller_signature", formSellerSignature.c_str());
                    }

                    if (Action == "feedback") {
                        CheckKeyForNull("customer_signature", formCustomerSignature.c_str());
                    }

                    YAML::Node Deal = Node["deal"];

                    Deal["order"] = Action.c_str();
                    Deal["type"] = formType.c_str();

                    Deal["at"] = formAt.c_str();
                    Deal["date"] = formDate.IsEmpty() ? UTCFormat(DateToString(UTC())).c_str() : formDate.c_str();
                    Deal["salt"] = formSalt.IsEmpty() ? GenSalt().c_str() : formSalt.c_str();

                    YAML::Node Seller = Deal["seller"];

                    Seller["address"] = formSellerAddress.c_str();

                    if (!formSellerRating.IsEmpty())
                        Seller["rating"] = formSellerRating.c_str();

                    if (!formSellerSignature.IsEmpty())
                        Seller["signature"] = formSellerSignature.c_str();

                    YAML::Node Customer = Deal["customer"];

                    Customer["address"] = formCustomerAddress.c_str();

                    if (!formSellerRating.IsEmpty())
                        Customer["rating"] = formCustomerRating.c_str();

                    if (!formCustomerSignature.IsEmpty())
                        Customer["signature"] = formCustomerSignature.c_str();

                    YAML::Node Payment = Deal["payment"];

                    if (!formPaymentAddress.IsEmpty())
                        Payment["address"] = formPaymentAddress.c_str();

                    if (!formPaymentUntil.IsEmpty())
                        Payment["until"] = formPaymentUntil.c_str();

                    Payment["sum"] = formPaymentSum.c_str();

                    if (!formFeedbackLeaveBefore.IsEmpty()) {
                        YAML::Node Feedback = Deal["feedback"];

                        Feedback["leave-before"] = formFeedbackLeaveBefore.c_str();

                        if (!formFeedbackStatus.IsEmpty())
                            Feedback["status"] = formFeedbackStatus.c_str();

                        if (!formFeedbackComments.IsEmpty())
                            Feedback["comments"] = formFeedbackComments.c_str();

                        if (!formFeedbackRefund.IsEmpty())
                            Feedback["refund"] = formFeedbackRefund.c_str();
                    }

                } else if (ContentType.Find("application/json") == 0) {

                    const CJSON jsonData(caServerRequest.Content);

                    const auto &formOrder = jsonData["order"].AsString().Lower();
                    const auto &formType = jsonData["type"].AsString();

                    const auto &formAt = jsonData["at"].AsString();
                    const auto &formDate = jsonData["date"].AsString();
                    const auto &formSalt = jsonData["salt"].AsString();

                    const CJSONValue &jsonSeller = jsonData["seller"];

                    const auto &formSellerAddress = jsonSeller["address"].AsString();
                    const auto &formSellerRating = jsonSeller["rating"].AsString();
                    const auto &formSellerSignature = jsonSeller["signature"].AsString();

                    const CJSONValue &jsonCustomer = jsonData["customer"];

                    const auto &formCustomerAddress = jsonCustomer["address"].AsString();
                    const auto &formCustomerRating = jsonCustomer["rating"].AsString();
                    const auto &formCustomerSignature = jsonCustomer["signature"].AsString();

                    const CJSONValue &jsonPayment = jsonData["payment"];

                    const auto &formPaymentAddress = jsonPayment["address"].AsString();
                    const auto &formPaymentUntil = jsonPayment["until"].AsString();
                    const auto &formPaymentSum = jsonPayment["sum"].AsString();

                    const CJSONValue &jsonFeedback = jsonData["feedback"];

                    CString Key("leave_before");
                    if (!jsonFeedback.HasOwnProperty(Key))
                        Key = "leave-before";

                    const auto &formFeedbackLeaveBefore = jsonFeedback[Key].AsString();
                    const auto &formFeedbackStatus = jsonFeedback["status"].AsString();
                    const auto &formFeedbackComments = jsonFeedback["comments"].AsString();
                    const auto &formFeedbackRefund = jsonFeedback["refund"].AsString();

                    const auto &action = Action.IsEmpty() ? formOrder : Action;

                    CheckKeyForNull("order", action.c_str());
                    CheckKeyForNull("type", formType.c_str());
                    CheckKeyForNull("at", formAt.c_str());
                    CheckKeyForNull("seller.address", formSellerAddress.c_str());
                    CheckKeyForNull("customer.address", formCustomerAddress.c_str());
                    CheckKeyForNull("payment.sum", formPaymentSum.c_str());

                    if (action == "cancel") {
                        CheckKeyForNull("seller.signature", formSellerSignature.c_str());
                    }

                    if (action == "feedback") {
                        CheckKeyForNull("customer.signature", formCustomerSignature.c_str());
                    }

                    YAML::Node Deal = Node["deal"];

                    Deal["order"] = action.c_str();
                    Deal["type"] = formType.c_str();

                    Deal["at"] = formAt.c_str();
                    Deal["date"] = formDate.IsEmpty() ? UTCFormat(DateToString(UTC())).c_str() : formDate.c_str();
                    Deal["salt"] = formSalt.IsEmpty() ? GenSalt().c_str() : formSalt.c_str();

                    YAML::Node Seller = Deal["seller"];

                    Seller["address"] = formSellerAddress.c_str();

                    if (!formSellerSignature.IsEmpty())
                        Seller["signature"] = formSellerSignature.c_str();

                    if (!formSellerRating.IsEmpty())
                        Seller["rating"] = formSellerRating.c_str();

                    YAML::Node Customer = Deal["customer"];

                    Customer["address"] = formCustomerAddress.c_str();

                    if (!formCustomerSignature.IsEmpty())
                        Customer["signature"] = formCustomerSignature.c_str();

                    if (!formSellerRating.IsEmpty())
                        Customer["rating"] = formCustomerRating.c_str();

                    YAML::Node Payment = Deal["payment"];

                    if (!formPaymentAddress.IsEmpty())
                        Payment["address"] = formPaymentAddress.c_str();

                    if (!formPaymentUntil.IsEmpty())
                        Payment["until"] = formPaymentUntil.c_str();

                    Payment["sum"] = formPaymentSum.c_str();

                    if (!formFeedbackLeaveBefore.IsEmpty()) {
                        YAML::Node Feedback = Deal["feedback"];

                        Feedback["leave-before"] = formFeedbackLeaveBefore.c_str();

                        if (!formFeedbackStatus.IsEmpty())
                            Feedback["status"] = formFeedbackStatus.c_str();

                        if (!formFeedbackComments.IsEmpty())
                            Feedback["comments"] = formFeedbackComments.c_str();

                        if (!formFeedbackRefund.IsEmpty())
                            Feedback["refund"] = formFeedbackRefund.c_str();
                    }

                } else {
                    Node = YAML::Load(caServerRequest.Content.c_str());
                }
                
                const auto &BTCKeys = caContext.BTCKeys();

                if (BTCKeys.Count() < 2)
                    throw ExceptionFrm("Bitcoin keys cannot be empty.");

                for (int i = 0; i < BTCKeys.Count(); ++i) {
                    if (BTCKeys[i].IsEmpty())
                        throw ExceptionFrm("Bitcoin KEY%d cannot be empty.", i);
                }

                CDeal Deal(Node);

                auto& Data = Deal.Data();

                if (Data.Order == doCreate) {
                    const auto isSegWit = IsSegWitAddress(Data.Seller.Address) && IsSegWitAddress(Data.Customer.Address);

                    Data.Payment.Address = isSegWit ?
                            Deal.GetPaymentSW(BTCKeys.Names(0), BTCKeys.Names(1), Deal.Data().Transaction.Key, BitcoinConfig.version_hd) :
                            Deal.GetPaymentHD(BTCKeys.Names(0), BTCKeys.Names(1), Deal.Data().Transaction.Key, BitcoinConfig.version_hd, BitcoinConfig.version_script);

                    Node["deal"]["date"] = Data.Date.c_str();

                    YAML::Node Payment = Node["deal"]["payment"];
                    Payment.remove("sum");

                    Payment["address"] = Data.Payment.Address.c_str();
                    Payment["until"] = Data.Payment.Until.c_str();
                    Payment["sum"] = Data.Payment.Sum.c_str();

                    Node["deal"]["feedback"]["leave-before"] = Data.FeedBack.LeaveBefore.c_str();
                }

                CheckDeal(Deal);

                const CString caClearText(YAML::Dump(Node));

                if (pgpValue == "off" || pgpValue == "false") {
                    sPayload = caClearText;
                } else {
                    Apostol::PGP::CleartextSignature(
                            m_pgpModuleKey,
                            m_pgpPassphrase,
                            BPS_PGP_HASH,
                            caClearText,
                            sPayload);
                }
            }

            CJSON Json(jvtObject);
            CJSONValue Module(jvtObject);

            Module.Object().AddPair("address", caModuleAddress);
            Module.Object().AddPair("fee", caModuleFee);

            Json.Object().AddPair("id", GetUID(16).Lower());
            Json.Object().AddPair("host", caHost);

            if (!caOrigin.IsEmpty()) {
                Json.Object().AddPair("origin", caOrigin);
            }

            Json.Object().AddPair("module", Module);

            if (!caDealCode.IsEmpty()) {
                CJSONValue Deal(jvtObject);
                Deal.Object().AddPair("code", caDealCode);
                Json.Object().AddPair("deal", Deal);
            } else {
                Json.Object().AddPair("address", caUserAddress.IsEmpty() ? caModuleAddress : caUserAddress);
            }

            if (!sPayload.IsEmpty())
                Json.Object().AddPair("payload", base64_encode(sPayload));

            CLocation Location(caServer);

            pProxy->Host() = Location.hostname;
            pProxy->Port(Location.port == 0 ? BPS_SERVER_PORT : Location.port);

            ProxyRequest.Clear();

            ProxyRequest.Location = Location.hostname + caServerRequest.Location.pathname + caServerRequest.Location.search;
            ProxyRequest.CloseConnection = true;
            ProxyRequest.ContentType = CHTTPRequest::json;
            ProxyRequest.Content << Json;

            CHTTPRequest::Prepare(ProxyRequest, "POST", URI.c_str());

            ProxyRequest.Headers.Values("Host", ProxyRequest.Location.Host());

            ProxyRequest.AddHeader("Authorization", "Bearer " + caContext.Tokens()["access_token"]);

            if (!caModuleAddress.IsEmpty())
                ProxyRequest.AddHeader("Module-Address", caModuleAddress);

            if (!caModuleFee.IsEmpty())
                ProxyRequest.AddHeader("Module-Fee", caModuleFee);

            if (!caOrigin.IsEmpty())
                ProxyRequest.AddHeader("Origin", caOrigin);

            AConnection->TimeOutInterval(30 * 1000);
            AConnection->UpdateTimeOut(Now());

            AConnection->CloseConnection(false);

            pProxy->AutoFree(true);
            pProxy->UsedSSL(Location.protocol == HTTPS_PREFIX);
            pProxy->Active(true);
        }
        //--------------------------------------------------------------------------------------------------------------

        void CWebService::DoProxy(CHTTPServerConnection *AConnection, const CString &Method, const CString &URI) {

            const auto pProxy = GetProxy(AConnection);
            const auto &caServerRequest = AConnection->Request();
            auto &ProxyRequest = pProxy->Request();

            const auto &caModuleAddress = m_Module["address"];
            const auto &caModuleFee = m_Module["fee"];

            const auto checkFee = CheckFee(caModuleFee);
            if (checkFee == -1)
                throw ExceptionFrm("Invalid module fee value: %s", caModuleFee.c_str());

            const auto &caOrigin = caServerRequest.Headers["origin"];
            const auto &caContentType = caServerRequest.Headers["content-type"].Lower();

            const auto &caServerParam = GetParam(caServerRequest, "server");

            const auto index = CurrentContextIndex(caServerParam);

            const auto &caContext = index == -1 ? m_CurrentServer : m_Servers[index].Value();
            const auto &caServer = caContext.URL().Origin();

            CLocation Location(caServer);

            pProxy->Host() = Location.hostname;
            pProxy->Port(Location.port == 0 ? BPS_SERVER_PORT : Location.port);

            ProxyRequest.Clear();

            ProxyRequest.Location = Location.hostname + caServerRequest.Location.pathname + caServerRequest.Location.search;

            if (Method == "POST") {
                ProxyRequest.ContentType = StringToContentType(caContentType);
                ProxyRequest.Content = caServerRequest.Content;
            }

            ProxyRequest.CloseConnection = true;

            CHTTPRequest::Prepare(ProxyRequest, Method.c_str(), URI.c_str(), caContentType.c_str());

            ProxyRequest.Headers.Values("Host", ProxyRequest.Location.Host());

            ProxyRequest.AddHeader("Authorization", "Bearer " + caContext.Tokens()["access_token"]);

            if (!caModuleAddress.IsEmpty())
                ProxyRequest.AddHeader("Module-Address", caModuleAddress);

            if (!caModuleFee.IsEmpty())
                ProxyRequest.AddHeader("Module-Fee", caModuleFee);

            if (!caOrigin.IsEmpty())
                ProxyRequest.AddHeader("Origin", caOrigin);

            AConnection->TimeOutInterval(15 * 1000);
            AConnection->UpdateTimeOut(Now());

            AConnection->CloseConnection(false);

            pProxy->AutoFree(true);
            pProxy->UsedSSL(Location.protocol == HTTPS_PREFIX);
            pProxy->Active(true);
        }
        //--------------------------------------------------------------------------------------------------------------

        void CWebService::DoSignature(CHTTPServerConnection *AConnection) {
            const auto &caRequest = AConnection->Request();
            auto &Reply = AConnection->Reply();

            if (caRequest.Content.IsEmpty()) {
                AConnection->SendStockReply(CHTTPReply::no_content);
                return;
            }

            const auto &caServerParam = caRequest.Params["server"];
            int index = CurrentContextIndex(caServerParam);
            const auto &caContext = index == -1 ? m_CurrentServer : m_Servers[index].Value();

            auto &Keys = caContext.PGP();

            index = 0;
            while (index < Keys.Count() && Keys[index].Name != caContext.Name()) {
                index++;
            }

            if (index == Keys.Count())
                throw ExceptionFrm("PGP key not found.");

            const auto &caServerKey = Keys[index].Key;

            if (caServerKey.IsEmpty())
                throw ExceptionFrm("PGP key not added.");

            CString message;
            CJSON Json(jvtObject);

            const auto &caContentType = caRequest.Headers["content-type"];

            if (caContentType.Find("application/x-www-form-urlencoded") == 0) {
                const auto &FormData = caRequest.FormData;

                const auto &caClearText = FormData["message"];
                CheckKeyForNull("message", caClearText.c_str());

                const auto bVerified = CheckVerifyPGPSignature(VerifyPGPSignature(caClearText, caServerKey, message), message);
                Json.Object().AddPair("verified", bVerified);
            } else if (caContentType.Find("multipart/form-data") == 0) {
                CFormData FormData;
                CHTTPRequestParser::ParseFormData(caRequest, FormData);

                const auto &caClearText = FormData.Data("message");
                CheckKeyForNull("message", caClearText.c_str());

                const auto bVerified = CheckVerifyPGPSignature(VerifyPGPSignature(caClearText, caServerKey, message), message);
                Json.Object().AddPair("verified", bVerified);
            } else if (caContentType.Find("application/json") == 0) {
                const CJSON jsonData(caRequest.Content);

                const auto &caClearText = jsonData["message"].AsString();
                CheckKeyForNull("message", caClearText.c_str());

                const auto bVerified = CheckVerifyPGPSignature(VerifyPGPSignature(caClearText, caServerKey, message), message);
                Json.Object().AddPair("verified", bVerified);
            } else {
                const auto &caClearText = caRequest.Content;
                const auto bVerified = CheckVerifyPGPSignature(VerifyPGPSignature(caClearText, caServerKey, message), message);
                Json.Object().AddPair("verified", bVerified);
            }

            Json.Object().AddPair("message", message);

            AConnection->CloseConnection(true);
            Reply.Content << Json;

            AConnection->SendReply(CHTTPReply::ok);
        }
        //--------------------------------------------------------------------------------------------------------------

        void CWebService::FetchPGP(CContext &Context) {

            Log()->Notice("[%s] [%s] Trying to fetch a PGP key.", Context.Name().c_str(), Context.URL().Origin().c_str());

            auto OnRequest = [&Context](CHTTPClient *Sender, CHTTPRequest &Request) {

                const auto &name = Sender->Data()["name"];
                auto &Keys = Context.PGP();

                int index = 0;
                while (index < Keys.Count() && Keys[index].Name != name) {
                    index++;
                }

                Request.ContentType = CHTTPRequest::json;

                if (index < Keys.Count()) {
                    auto &Key = Keys[index];
                    Key.Status = CKeyContext::ksFetching;
                    Key.StatusTime = Now();
                }

                Request.Content = CString().Format(R"({"type": "pgp", "account": "%s", "code": "%s", "fields": ["code", "data"]})",  Context.Name().c_str(), name.c_str());

                CHTTPRequest::Prepare(Request, "POST", "/api/v1/key/public");

                const auto &access_token = Context.Tokens()["access_token"];

                if (!access_token.empty()) {
                    Request.AddHeader("Authorization", "Bearer " + access_token);
                }

                DebugRequest(Request);
            };

            auto OnExecute = [this, &Context](CTCPConnection *AConnection) {

                const auto pConnection = dynamic_cast<CHTTPClientConnection *> (AConnection);

                if (pConnection != nullptr) {
                    const auto pClient = dynamic_cast<CHTTPClient *> (pConnection->Client());
                    const auto &Reply = pConnection->Reply();

                    DebugReply(Reply);

                    CString Code;
                    CString Key;

                    try {
                        JsonStringToKey(Reply.Content, Code, Key);

                        if (Key.IsEmpty())
                            throw Delphi::Exception::Exception("Not found.");

                        const auto &name = pClient->Data()["name"];

                        auto &Keys = Context.PGP();

                        int index = 0;
                        while (index < Keys.Count() && Keys[index].Name != name) {
                            index++;
                        }

                        if (index < Keys.Count()) {
                            auto &PGP = Keys[index];

                            PGP.Status = CKeyContext::ksSuccess;
                            PGP.StatusTime = Now();
                            PGP.Key = Key;

                            if (Code == "PUBLIC") {
                                UpdateServerList(m_Servers, Key);
                                Context.SetStatus(csRunning);
                            }
                        }
                    } catch (Delphi::Exception::Exception &e) {
                        Context.SetFixedDate(Context.FixedDate() + (CDateTime) 5 / MinsPerDay); // 5 min
                        Context.SetStatus(Context::csAuthorized);

                        Log()->Error(APP_LOG_INFO, 0, "[FetchPGP] %s", e.what());
                    }

                    pConnection->CloseConnection(true);
                }

                return true;
            };

            auto OnException = [&Context](CTCPConnection *AConnection, const Delphi::Exception::Exception &E) {
                const auto pConnection = dynamic_cast<CHTTPClientConnection *> (AConnection);
                if (pConnection != nullptr) {
                    const auto pClient = dynamic_cast<CHTTPClient *> (pConnection->Client());
                    if (pClient != nullptr) {
                        const auto &name = pClient->Data()["name"];
                        auto &Keys = Context.PGP();

                        int index = 0;
                        while (index < Keys.Count() && Keys[index].Name != name) {
                            index++;
                        }

                        if (index < Keys.Count()) {
                            auto &Key = Keys[index];
                            Key.Status = CKeyContext::ksError;
                            Key.StatusTime = Now();
                        }

                        Log()->Error(APP_LOG_EMERG, 0, "[%s:%d] %s", pClient->Host().c_str(), pClient->Port(), E.what());
                    }
                    DebugReply(pConnection->Reply());
                }

                Context.SetStatus(Context::csAuthorized);
            };

            auto &Keys = Context.PGP();

            for (int i = 0; i < Keys.Count(); i++ ) {
                auto &Key = Keys[i];

                Key.Status = CKeyContext::ksUnknown;
                Key.StatusTime = Now();
                Key.RunTime = Key.StatusTime;

                const auto pClient = GetClient(Context.URL().hostname, Context.URL().port == 0 ? BPS_SERVER_PORT : Context.URL().port);

                pClient->Data().AddPair("name", Key.Name);

                pClient->OnRequest(OnRequest);
                pClient->OnExecute(OnExecute);
                pClient->OnException(OnException);

                pClient->AutoFree(true);
                pClient->Active(true);
            }
        }
        //--------------------------------------------------------------------------------------------------------------

        void CWebService::Reload() {
            Config()->IniFile().ReadSectionValues("module", &m_Module);

            CString FileName;

            FileName = Config()->IniFile().ReadString(CONFIG_SECTION_NAME, "oauth2", "oauth2/service.json");
            if (!path_separator(FileName.front())) {
                FileName = Config()->Prefix() + FileName;
            }

            if (FileExists(FileName.c_str())) {
                m_OAuth2.LoadFromFile(FileName.c_str());
            }

            const auto &caPrivateKey = Config()->IniFile().ReadString("pgp", "private", "module.sec");
            const auto &caPublicKey = Config()->IniFile().ReadString("pgp", "public", "dm.pub");

            m_pgpPassphrase = Config()->IniFile().ReadString("pgp", "passphrase", "");

            if (FileExists(caPrivateKey.c_str())) {
                m_pgpModuleKey.LoadFromFile(caPrivateKey.c_str());

                if (FileExists(caPublicKey.c_str())) {
                    m_pgpPublicKey.LoadFromFile(caPublicKey.c_str());
                    UpdateServerList(m_Servers, m_pgpPublicKey);
                } else {
                    Log()->Error(APP_LOG_WARN, 0, APP_FILE_NOT_FOUND, caPublicKey.c_str());
                }
            } else {
                Log()->Error(APP_LOG_WARN, 0, APP_FILE_NOT_FOUND, caPrivateKey.c_str());
            }
        }
        //--------------------------------------------------------------------------------------------------------------

        void CWebService::Heartbeat(CDateTime Now) {
            for (int i = 0; i < m_Servers.Count(); i++) {
                auto &Context = m_Servers[i].Value();

                if (Now >= Context.CheckDate()) {
                    Context.SetCheckDate(Now + (CDateTime) 30 / SecsPerDay); // 30 sec

                    if (Context.Status() == Context::csInitialization) {
                        ModuleService(Context);
                    }

                    if (Context.Status() >= Context::csInitialized) {
                        CheckProviders(Context, Now);
                    }
                }

                if (Now >= Context.FixedDate()) {
                    if (Context.Status() == Context::csAuthorized) {
                        Context.SetFixedDate(Now + (CDateTime) 30 / SecsPerDay); // 30 sec
                        Context.SetStatus(Context::csInProgress);

                        FetchPGP(Context);
                    }
                }

                if (Context.Status() == Context::csRunning) {
                    if (Now >= m_NexServerTime) {
                        m_NexServerTime = GetRandomDate(10 * 60, m_SyncPeriod * 60, Now);
                        m_CurrentServer = Context;
                    }
                }
            }
        }
        //--------------------------------------------------------------------------------------------------------------

    }
}
}