/*
 * Copyright (c) Contributors to the Open 3D Engine Project.
 * For complete copyright and license terms please see the LICENSE at the root of this distribution.
 *
 * SPDX-License-Identifier: Apache-2.0 OR MIT
 *
 */

#pragma once


#include <AzCore/std/functional.h>
#include <AzCore/std/string/regex.h>
#include <AzCore/std/string/tokenize.h>
#include <AzCore/JSON/document.h>
#include <AzCore/JSON/prettywriter.h>
#include <AzCore/Component/TickBus.h>

#include <Framework/ServiceClientJob.h>
#include <Framework/JsonObjectHandler.h>
#include <Framework/JsonWriter.h>
#include <Framework/Error.h>
#include <Framework/ServiceRequestJobConfig.h>
#include <Framework/RequestBuilder.h>
#include <Framework/ServiceJobUtil.h>

// The AWS Native SDK AWSAllocator triggers a warning due to accessing members of std::allocator directly.
// AWSAllocator.h(70): warning C4996: 'std::allocator<T>::pointer': warning STL4010: Various members of std::allocator are deprecated in C++17.
// Use std::allocator_traits instead of accessing these members directly.
// You can define _SILENCE_CXX17_OLD_ALLOCATOR_MEMBERS_DEPRECATION_WARNING or _SILENCE_ALL_CXX17_DEPRECATION_WARNINGS to acknowledge that you have received this warning.

AZ_PUSH_DISABLE_WARNING(4251 4996, "-Wunknown-warning-option")
#include <aws/core/http/HttpResponse.h>
#include <aws/core/auth/AWSAuthSigner.h>
#include <aws/core/auth/AWSCredentialsProvider.h>
AZ_POP_DISABLE_WARNING


namespace AWSCore
{
    const char logRequestsChannel[] = "ServiceRequest";
    /// Base class for service requests. To use, derive a class and
    /// then provide that class as the argument to the ServiceRequestJob
    /// template class.
    ///
    /// This class provide defaults, but many of these need to be 
    /// overridden in the derived type for most requests, and ServiceTraits
    /// must be overridden for all requests. Use the SERVICE_REQUEST
    /// macro to implement the common overrides.
    class ServiceRequest
    {

    public:

        /// Macro used in derived classes to perform the common overrides.
#define SERVICE_REQUEST(SERVICE_NAME, METHOD, PATH) \
                using ServiceTraits = SERVICE_NAME##ServiceTraits; \
                static const char* Path() { return PATH; } \
                static HttpMethod Method() { return METHOD; }

        /// ServiceTraits must be overridden by the derived type.
        using ServiceTraits = void;

        using HttpMethod = Aws::Http::HttpMethod;

        /// Must be overridden if the request method is not GET.
        static HttpMethod Method()
        {
            return HttpMethod::HTTP_GET;
        }

        /// Must be overridden if the request requires an URL path.
        /// By default the service url alone will be used.
        static const char* Path()
        {
            return ""; // no path
        }

        /// Type used for request parameters. If the request has 
        /// parameters, define a parameters type and use it to 
        /// override the parameters member.
        struct NoParameters
        {

            bool BuildRequest(AWSCore::RequestBuilder& request)
            {
                AZ_UNUSED(request);
                return true; // ok
            }

        };

        /// Stores parameter values. Must be overridden if the
        /// request has parameters.
        NoParameters parameters;

        /// Type used for result data. If the request has result data,
        /// define a result type and use it to override the result member.
        struct EmptyResult
        {

            bool OnJsonKey(const char* key, AWSCore::JsonReader& reader)
            {
                AZ_UNUSED(key);
                AZ_UNUSED(reader);

                return reader.Ignore();
            }

        };

        /// Stores result data. Must be overridden if the request has 
        /// result data.
        EmptyResult result;

        /// Stores error information should the request fail. There is no 
        /// need to override this member and doing so will waste a little
        /// memory.
        Error error;

        /// Determines if the AWS credentials, as supplied by the credentialsProvider from
        /// the ServiceRequestJobConfig object (which defaults to the user's credentials), 
        /// are used to sign the request. The default is true. Override this and return false 
        /// if calling a public API and want to avoid the overhead of signing requests.
        bool UseAWSCredentials() {
            return true;
        }

    };

    /// Base class for Cloud Gem service request jobs.
    template<class RequestType>
    class ServiceRequestJob
        : public ServiceClientJob<typename RequestType::ServiceTraits>
        , public RequestType
    {

    public:
        // To use a different allocator, extend this class and use this macro.
        AZ_CLASS_ALLOCATOR(ServiceRequestJob, AZ::SystemAllocator);

        /// Aliases for the configuration types used for this job class.
        using IConfig = IServiceRequestJobConfig;
        using Config = ServiceRequestJobConfig<RequestType>;

        /// An alias for this type, useful in derived classes.
        using ServiceRequestJobType = ServiceRequestJob<RequestType>;
        using ServiceClientJobType = ServiceClientJob<typename RequestType::ServiceTraits>;

        using OnSuccessFunction = AZStd::function<void(ServiceRequestJob* job)>;
        using OnFailureFunction = AZStd::function<void(ServiceRequestJob* job)>;

        class Function;

        template<class Allocator = AZ::SystemAllocator>
        static ServiceRequestJob* Create(OnSuccessFunction onSuccess, OnFailureFunction onFailure = OnFailureFunction{}, IConfig* config = GetDefaultConfig());

        static Config* GetDefaultConfig()
        {
            static AwsApiJobConfigHolder<Config> s_configHolder{};
            return s_configHolder.GetConfig(ServiceClientJobType::GetDefaultConfig());
        }

        ServiceRequestJob(bool isAutoDelete, IConfig* config = GetDefaultConfig())
            : ServiceClientJobType{ isAutoDelete, config }
            , m_requestUrl{ config->GetRequestUrl() }
        {
            if (RequestType::UseAWSCredentials())
            {
                if (!HasCredentials(config))
                {
                    m_missingCredentials = true;
                    return;
                }

                m_AWSAuthSigner.reset(
                    new Aws::Client::AWSAuthV4Signer{
                        config->GetCredentialsProvider(),
                        "execute-api",
                        DetermineRegionFromRequestUrl()
                    }
                );
            }
        }

        bool HasCredentials(IConfig* config)
        {
            if (config == nullptr)
            {
                return false;
            }

            if (!config->GetCredentialsProvider())
            {
                return false;
            }

            auto awsCredentials = config->GetCredentialsProvider()->GetAWSCredentials();

            return !(awsCredentials.GetAWSAccessKeyId().empty() || awsCredentials.GetAWSSecretKey().empty());
        }

        /// Returns true if no error has occurred.
        bool WasSuccess()
        {
            return RequestType::error.type.empty();
        }

        /// Override AZ:Job defined method to reset request state when
        /// the job object is reused.
        void Reset(bool isClearDependent) override
        {
            RequestType::parameters = decltype(RequestType::parameters)();
            RequestType::result = decltype(RequestType::result)();
            RequestType::error = decltype(RequestType::error)();
            ServiceClientJobType::Reset(isClearDependent);
        }

    protected:
        /// Called to prepare the request. By default no changes
        /// are made to the parameters object. Override to defer the preparation
        /// of parameters until running on the job's worker thread,
        /// instead of setting parameters before calling Start.
        ///
        /// \return true if the request was prepared successfully.
        virtual bool PrepareRequest()
        {
            return IsValid();
        }

        virtual bool IsValid() const
        {
            return m_requestUrl.length() && !m_missingCredentials;
        }

        /// Called when a request completes without error.
        virtual void OnSuccess()
        {
        }

        /// Called when an error occurs.
        virtual void OnFailure()
        {
        }

        /// Provided so derived functions that do not auto delete can clean up
        virtual void DoCleanup()
        {
        }

        /// The URL created by appending the API path to the service URL.
        /// The path may contain {param} format parameters. The
        /// RequestType::parameters.BuildRequest method is responsible
        /// for replacing these parts of the url.
        const Aws::String& m_requestUrl;

        std::shared_ptr<Aws::Client::AWSAuthV4Signer> m_AWSAuthSigner{ nullptr };

        // Passed in configuration contains the AWS Credentials to use. If this request requires credentials
        // check in the constructor and set this bool to indicate if we're not valid before placing the credentials
        // in the m_AWSAuthSigner
        bool m_missingCredentials{ false };

    private:
        bool BuildRequest(RequestBuilder& request) override
        {
            bool ok = PrepareRequest();
            if (ok)
            {
                request.SetHttpMethod(RequestType::Method());
                request.SetRequestUrl(m_requestUrl);
                request.SetAWSAuthSigner(m_AWSAuthSigner);
                ok = RequestType::parameters.BuildRequest(request);
                if (!ok)
                {
                    RequestType::error.type = Error::TYPE_CONTENT_ERROR;
                    RequestType::error.message = request.GetErrorMessage();
                    OnFailure();
                }
            }
            return ok;
        }

        AZStd::string EscapePercentCharsInString(const AZStd::string& str) const
        {
            return AZStd::regex_replace(str, AZStd::regex("%"), "%%");
        }

        void ProcessResponse(const std::shared_ptr<Aws::Http::HttpResponse>& response) override
        {
            if (ServiceClientJobType::IsCancelled())
            {
                RequestType::error.type = Error::TYPE_NETWORK_ERROR;
                RequestType::error.message = "Job canceled while waiting for a response.";
            }
            else if (!response)
            {
                RequestType::error.type = Error::TYPE_NETWORK_ERROR;
                RequestType::error.message = "An unknown error occurred while making the request.";
            }
            else
            {

#ifdef _DEBUG
                // Code assumes application/json; charset=utf-8
                const Aws::String& contentType = response->GetContentType();
                AZ_Error(
                    ServiceClientJobType::COMPONENT_DISPLAY_NAME,
                    contentType.find("application/json") != AZStd::string::npos &&
                    (contentType.find("charset") == AZStd::string::npos ||
                        contentType.find("utf-8") != AZStd::string::npos),
                    "Service response content type is not application/json; charset=utf-8: %s", contentType.c_str()
                );
#endif

                int responseCode = static_cast<int>(response->GetResponseCode());
                Aws::IOStream& responseBody = response->GetResponseBody();
#ifdef _DEBUG
                std::istreambuf_iterator<AZStd::string::value_type> eos;
                AZStd::string responseContent = AZStd::string{ std::istreambuf_iterator<AZStd::string::value_type>(responseBody),eos };
                AZ_Printf(ServiceClientJobType::COMPONENT_DISPLAY_NAME, "Processing %d response: %s.", responseCode, responseContent.c_str());
                responseBody.clear();
                responseBody.seekg(0);
#endif
                static AZ::EnvironmentVariable<bool> envVar = AZ::Environment::FindVariable<bool>("AWSLogVerbosity");

                if (envVar && envVar.Get())
                {
                    ShowRequestLog(response);
                }
                JsonInputStream stream{ responseBody };

                if (responseCode >= 200 && responseCode <= 299)
                {
                    ReadResponseObject(stream);
                }
                else
                {
                    ReadErrorObject(responseCode, stream);
                }

            }

            if (WasSuccess())
            {
                OnSuccess();
            }
            else
            {

                AZStd::string requestContent;
                AZStd::string responseContent;
                if (response)
                {

                    // Get request and response content. Not attempting to do any charset
                    // encoding/decoding, so the display may not be correct but it should 
                    // work fine for the usual ascii and utf8 content.

                    std::istreambuf_iterator<AZStd::string::value_type> eos;

                    std::shared_ptr<Aws::IOStream> requestStream = response->GetOriginatingRequest().GetContentBody();
                    if (requestStream)
                    {
                        requestStream->clear();
                        requestStream->seekg(0);
                        requestContent = AZStd::string{ std::istreambuf_iterator<AZStd::string::value_type>(*requestStream.get()),eos };
                        // Replace the character "%" with "%%" to prevent the error when printing the string that contains the percentage sign
                        requestContent = EscapePercentCharsInString(requestContent);
                    }

                    Aws::IOStream& responseStream = response->GetResponseBody();
                    responseStream.clear();
                    responseStream.seekg(0);
                    responseContent = AZStd::string{ std::istreambuf_iterator<AZStd::string::value_type>(responseStream),eos };

                }

#if defined(AZ_ENABLE_TRACING)
                AZStd::string message = AZStd::string::format("An %s error occurred when performing %s %s on service %s using %s: %s\n\nRequest Content:\n%s\n\nResponse Content:\n%s\n\n",
                    RequestType::error.type.c_str(),
                    ServiceRequestJobType::HttpMethodToString(RequestType::Method()),
                    RequestType::Path(),
                    RequestType::ServiceTraits::ServiceName,
                    response ? response->GetOriginatingRequest().GetURIString().c_str() : "NULL",
                    RequestType::error.message.c_str(),
                    requestContent.c_str(),
                    responseContent.c_str()
                );

                // This is determined by AZ::g_maxMessageLength defined in in dev\Code\Framework\AzCore\AzCore\Debug\Trace.cpp.
                // It has the value 4096, but there is the timestamp, etc., to account for so we reduce it by a few characters.
                const int MAX_MESSAGE_LENGTH = 4096 - 128;

                // Replace the character "%" with "%%" to prevent the error when printing the string that contains the percentage sign
                message = EscapePercentCharsInString(message);
                if (message.size() > MAX_MESSAGE_LENGTH)
                {
                    int offset = 0;
                    while (offset < message.size())
                    {
                        int count = static_cast<int>((offset + MAX_MESSAGE_LENGTH < message.size()) ? MAX_MESSAGE_LENGTH : message.size() - offset);
                        AZ_Warning(ServiceClientJobType::COMPONENT_DISPLAY_NAME, false, message.substr(offset, count).c_str());
                        offset += MAX_MESSAGE_LENGTH;
                    }

                }
                else
                {
                    AZ_Warning(ServiceClientJobType::COMPONENT_DISPLAY_NAME, false, message.c_str());
                }
#endif

                OnFailure();

            }

        }

        void ReadErrorObject(int responseCode, JsonInputStream& stream)
        {
            AZStd::string parseErrorMessage;
            bool ok = JsonReader::ReadObject(stream, RequestType::error, parseErrorMessage);
            ok = ok && !RequestType::error.message.empty();
            ok = ok && !RequestType::error.type.empty();
            if (!ok)
            {
                if (responseCode < 400)
                {
                    // Not expected to make it here: 100 (info), 200 (success), or 300 (redirect).
                    RequestType::error.type = Error::TYPE_CONTENT_ERROR;
                    RequestType::error.message = AZStd::string::format("Unexpected response code %i received. %s", responseCode, stream.GetContent().c_str());
                }
                else if (responseCode < 500)
                {
                    RequestType::error.type = Error::TYPE_CLIENT_ERROR;
                    switch (responseCode)
                    {
                    case 401:
                    case 403:
                        RequestType::error.message = AZStd::string::format("Access denied (%i). %s", responseCode, stream.GetContent().c_str());
                        break;
                    case 404:
                        RequestType::error.message = AZStd::string::format("Not found (%i). %s", responseCode, stream.GetContent().c_str());
                        break;
                    case 405:
                        RequestType::error.message = AZStd::string::format("Method not allowed (%i). %s", responseCode, stream.GetContent().c_str());
                        break;
                    case 406:
                        RequestType::error.message = AZStd::string::format("Content not acceptable (%i). %s", responseCode, stream.GetContent().c_str());
                        break;
                    default:
                        RequestType::error.message = AZStd::string::format("Client error (%i). %s", responseCode, stream.GetContent().c_str());
                        break;
                    }
                }
                else if (responseCode < 600)
                {
                    RequestType::error.type = Error::TYPE_SERVICE_ERROR;
                    RequestType::error.message = AZStd::string::format("Service error (%i). %s", responseCode, stream.GetContent().c_str());
                }
                else
                {
                    // Anything above 599 isn't valid HTTP.
                    RequestType::error.type = Error::TYPE_CONTENT_ERROR;
                    RequestType::error.message = AZStd::string::format("Unexpected response code %i received. %s", responseCode, stream.GetContent().c_str());
                }

            }
        }

        /// Parses a JSON object from a stream and writes the values found to a
        /// provided object. ResultObjectType should implement the following function:
        ///
        ///     void OnJsonKey(const char* key, JsonObjectHandler& handler)
        ///
        /// This function will be called for each of the object's properties. It should 
        /// call one of the Expect methods on the state object to identify the expected
        /// property type and provide a location where the property value can be stored.
        void ReadResponseObject(JsonInputStream& stream)
        {
            JsonKeyHandler objectKeyHandler = JsonReader::GetJsonKeyHandler(RequestType::result);
            JsonKeyHandler responseKeyHandler = GetResponseObjectKeyHandler(objectKeyHandler);
            bool ok = JsonReader::ReadObject(stream, responseKeyHandler, RequestType::error.message);
            if (!ok)
            {
                RequestType::error.type = Error::TYPE_CONTENT_ERROR;
            }
        }

        /// Creates the JsonKeyHandler function used by ReadResultObject to 
        /// process the response body received from the service. The 
        /// response content is determined by the response mappings 
        /// used to configure API Gateway. The response is expected to be a 
        /// JSON object with, at minimum, an "result" property.
        ///
        /// Can extend response properties in the swagger/OpenAPI spec and provide a handler for
        /// those properties by implementing GetResponseObjectKeyHandler. For
        /// example, it may be useful to return the API Gateway generated 
        /// request id, which can help when trying to diagnose problems.
        JsonKeyHandler GetResponseObjectKeyHandler(JsonKeyHandler resultKeyHandler)
        {

            return [resultKeyHandler](const char* key, JsonReader& reader)
            {
                if (strcmp(key, "result") == 0) return reader.Accept(resultKeyHandler);
                return reader.Ignore();
            };

        }

        Aws::String DetermineRegionFromRequestUrl()
        {
            Aws::String region = DetermineRegionFromServiceUrl(m_requestUrl);
            if (region.empty())
            {
                AZ_Warning(
                    ServiceClientJobType::COMPONENT_DISPLAY_NAME,
                    false,
                    "Service request url %s does not have the expected format. Cannot determine region from the url.",
                    m_requestUrl.c_str()
                );

                region = "us-east-1";
            }
            return region;
        }

        static AZStd::string GetFormattedJSON(const AZStd::string& inputStr)
        {
            rapidjson::Document jsonRep;
            jsonRep.Parse(inputStr.c_str());
            if (jsonRep.HasParseError())
            {
                // If input couldn't be parsed, just return as is so it'll be printed
                return inputStr;
            }

            rapidjson::StringBuffer buffer;
            rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
            jsonRep.Accept(writer);

            return buffer.GetString();
        }

        // If request output is longer than allowed, let's just break it apart during printing
        void PrintRequestOutput(const AZStd::string& outputStr)
        {
            AZStd::size_t startPos = 0;

            while (startPos < outputStr.length())
            {
                AZStd::size_t endPos = outputStr.find_first_of('\n', startPos);

                if (endPos == AZStd::string::npos)
                {
                    AZ_Printf(logRequestsChannel, outputStr.substr(startPos).c_str());
                    break;
                }
                else
                {
                    AZ_Printf(logRequestsChannel, outputStr.substr(startPos, endPos - startPos).c_str());
                    startPos = endPos + 1;
                }
            }
        }

        void ShowRequestLog(const std::shared_ptr<Aws::Http::HttpResponse>& response)
        {
            if (!response)
            {
                return;
            }

            AZStd::string requestContent;

            std::shared_ptr<Aws::IOStream> requestStream = response->GetOriginatingRequest().GetContentBody();
            if (requestStream)
            {
                std::istreambuf_iterator<AZStd::string::value_type> eos;
                requestStream->clear();
                requestStream->seekg(0);
                requestContent = AZStd::string{ std::istreambuf_iterator<AZStd::string::value_type>(*requestStream.get()),eos };
                // Replace the character "%" with "%%" to prevent the error when printing the string that contains the percentage sign
                requestContent = EscapePercentCharsInString(requestContent);
                requestContent = GetFormattedJSON(requestContent);
            }

            std::istreambuf_iterator<AZStd::string::value_type> responseEos;
            Aws::IOStream& responseStream = response->GetResponseBody();
            responseStream.clear();
            responseStream.seekg(0);
            AZStd::string responseContent = AZStd::string{ std::istreambuf_iterator<AZStd::string::value_type>(responseStream), responseEos };
            responseContent = EscapePercentCharsInString(responseContent);
            responseStream.seekg(0);

            responseContent = GetFormattedJSON(responseContent);

            AZ_Printf(logRequestsChannel, "Service Request Complete");
            AZ_Printf(logRequestsChannel, "Service: %s  URI : %s", RequestType::ServiceTraits::ServiceName, response ? response->GetOriginatingRequest().GetURIString().c_str() : "NULL");
            AZ_Printf(logRequestsChannel, "Request: %s %s", ServiceRequestJobType::HttpMethodToString(RequestType::Method()), RequestType::Path());
            PrintRequestOutput(requestContent);

            AZ_Printf(logRequestsChannel, "Got Response Code: %d", static_cast<int>(response->GetResponseCode()));
            AZ_Printf(logRequestsChannel, "Response Body:\n");
            PrintRequestOutput(responseContent);
        }
    };

    /// A derived class that calls lambda functions on job completion.
    template<class RequestTraits>
    class ServiceRequestJob<RequestTraits>::Function
        : public ServiceRequestJob
    {
    public:
        // To use a different allocator, extend this class and use this macro.
        AZ_CLASS_ALLOCATOR(Function, AZ::SystemAllocator);

        Function(OnSuccessFunction onSuccess, OnFailureFunction onFailure = OnFailureFunction{}, IConfig* config = GetDefaultConfig())
            : ServiceRequestJob(false, config) // No auto delete - The Function class will handle it with the DoCleanup() function
            , m_onSuccess{ onSuccess }
            , m_onFailure{ onFailure }
        {
        }

    private:
        void OnSuccess() override
        {
            AZStd::function<void()> callbackHandler = [this]()
            {
                if (m_onSuccess)
                {
                    m_onSuccess(this);
                }
                delete this;
            };
            AZ::TickBus::QueueFunction(callbackHandler);
        }

        void OnFailure() override
        {
            AZStd::function<void()> callbackHandler = [this]()
            {
                if (m_onFailure)
                {
                    m_onFailure(this);
                }
                delete this;
            };
            AZ::TickBus::QueueFunction(callbackHandler);
        }

        // Code doesn't use auto delete - this ensure things get cleaned up in cases when code can't call success or failure
        void DoCleanup() override
        {
            AZStd::function<void()> callbackHandler = [this]()
            {
                delete this;
            };
            AZ::TickBus::QueueFunction(callbackHandler);
        }

        OnSuccessFunction m_onSuccess;
        OnFailureFunction m_onFailure;

    };

    template<class RequestType>
    template<class Allocator>
    ServiceRequestJob<RequestType>* ServiceRequestJob<RequestType>::Create(
        OnSuccessFunction onSuccess, OnFailureFunction onFailure, IConfig* config)
    {
        return azcreate(Function, (onSuccess, onFailure, config), Allocator);
    }
} // namespace AWSCore


