/**
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

#include <aws/core/endpoint/DefaultEndpointProvider.h>
#include <aws/core/utils/memory/stl/AWSMap.h>
#include <aws/crt/Api.h>

namespace Aws
{
namespace Endpoint
{

/**
 * Export endpoint provider symbols from DLL
 */
template class AWS_CORE_API DefaultEndpointProvider<Aws::Client::GenericClientConfiguration<false>,
            Aws::Endpoint::BuiltInParameters,
            Aws::Endpoint::ClientContextParameters>;

char CharToDec(const char c)
{
    if(c >= '0' && c <= '9')
        return c - '0';
    if(c >= 'A' && c <= 'F')
        return c - 'A' + 10;
    if(c >= 'a' && c <= 'f')
        return c - 'a' + 10;
    return 0;
}

Aws::String PercentDecode(Aws::String inputString)
{
    if (inputString.find_first_of("%") == Aws::String::npos)
    {
        return inputString;
    }
    Aws::String result;
    result.reserve(inputString.size());

    bool percentFound = false;
    char firstOctet = 0;
    char secondOctet = 0;
    for(size_t i = 0; i < inputString.size(); ++i)
    {
        const char currentChar = inputString[i];
        if ('%' == currentChar)
        {
            if (percentFound)
            {
                // not percent-encoded string
                result += currentChar;
            }
            percentFound = true;
            continue;
        }

        if (percentFound)
        {
            if ((currentChar >= '0' && currentChar <= '9') ||
                (currentChar >= 'A' && currentChar <= 'F') ||
                (currentChar >= 'a' && currentChar <= 'f'))
            {
                if(!firstOctet)
                {
                    firstOctet = currentChar;
                    continue;
                }
                if(!secondOctet)
                {
                    secondOctet = currentChar;
                    char encodedChar = CharToDec(firstOctet) * 16 + CharToDec(secondOctet);
                    result += encodedChar;

                    percentFound = false;
                    firstOctet = 0;
                    secondOctet = 0;
                    continue;
                }
            } else {
                // Non-percent encoded sequence
                result += '%';
                if(!firstOctet)
                    result += firstOctet;
                result += currentChar;
                percentFound = false;
                firstOctet = 0;
                secondOctet = 0;
                continue;
            }
        }

        if ('+' == currentChar)
        {
            result += ' ';
            continue;
        }
        result += currentChar;
    }
    return result;
}

AWS_CORE_API ResolveEndpointOutcome
ResolveEndpointDefaultImpl(const Aws::Crt::Endpoints::RuleEngine& ruleEngine,
                           const EndpointParameters& builtInParameters,
                           const EndpointParameters& clientContextParameters,
                           const EndpointParameters& endpointParameters)
{
    if(!ruleEngine) {
        AWS_LOGSTREAM_FATAL(DEFAULT_ENDPOINT_PROVIDER_TAG, "Invalid CRT Rule Engine state");
        return ResolveEndpointOutcome(
                Aws::Client::AWSError<Aws::Client::CoreErrors>(
                        Aws::Client::CoreErrors::INTERNAL_FAILURE,
                        "",
                        "CRT Endpoint rule engine is not initialized",
                        false/*retryable*/));
    }

    Aws::Crt::Endpoints::RequestContext crtRequestCtx;

    const Aws::Vector<std::reference_wrapper<const EndpointParameters>> allParameters
            = {std::cref(builtInParameters), std::cref(clientContextParameters), std::cref(endpointParameters)};

    for (const auto& parameterClass : allParameters)
    {
        for(const auto& parameter : parameterClass.get())
        {
            if(EndpointParameter::ParameterType::BOOLEAN == parameter.GetStoredType())
            {
                AWS_LOGSTREAM_TRACE(DEFAULT_ENDPOINT_PROVIDER_TAG, "Endpoint bool eval parameter: " << parameter.GetName() << " = " << parameter.GetBoolValueNoCheck());
                crtRequestCtx.AddBoolean(Aws::Crt::ByteCursorFromCString(parameter.GetName().c_str()), parameter.GetBoolValueNoCheck());
            }
            else if(EndpointParameter::ParameterType::STRING == parameter.GetStoredType())
            {
                AWS_LOGSTREAM_TRACE(DEFAULT_ENDPOINT_PROVIDER_TAG, "Endpoint str eval parameter: " << parameter.GetName() << " = " << parameter.GetStrValueNoCheck());
                crtRequestCtx.AddString(Aws::Crt::ByteCursorFromCString(parameter.GetName().c_str()), Aws::Crt::ByteCursorFromCString(parameter.GetStrValueNoCheck().c_str()));
            }
            else
            {
                return ResolveEndpointOutcome(
                        Aws::Client::AWSError<Aws::Client::CoreErrors>(
                                Aws::Client::CoreErrors::INVALID_QUERY_PARAMETER,
                                "",
                                "Invalid endpoint parameter type for parameter " + parameter.GetName(),
                                false/*retryable*/));
            }
        }
    }

    auto resolved = ruleEngine.Resolve(crtRequestCtx);

    if(resolved.has_value())
    {
        if(resolved->IsError())
        {
            auto crtError = resolved->GetError();
            Aws::String sdkCrtError = crtError ? Aws::String(crtError->begin(), crtError->end()) :
                    "CRT Rule engine resolution resulted in an unknown error";
            return ResolveEndpointOutcome(
                    Aws::Client::AWSError<Aws::Client::CoreErrors>(
                            Aws::Client::CoreErrors::INVALID_PARAMETER_COMBINATION,
                            "",
                            sdkCrtError,
                            false/*retryable*/));
        }
        else if(resolved->IsEndpoint() && resolved->GetUrl())
        {
            Aws::Endpoint::AWSEndpoint endpoint;
            const auto crtUrl = resolved->GetUrl();
            Aws::String sdkCrtUrl = Aws::String(crtUrl->begin(), crtUrl->end());
            AWS_LOGSTREAM_DEBUG(DEFAULT_ENDPOINT_PROVIDER_TAG, "Endpoint rules engine evaluated the endpoint: " << sdkCrtUrl);
            endpoint.SetURL(PercentDecode(std::move(sdkCrtUrl)));

            // Transform attributes
            // Each attribute consist of properties, hence converting CRT properties to SDK attributes
            const auto crtProps = resolved->GetProperties();
            if (crtProps && crtProps->size() > 2) {
                Aws::String sdkCrtProps = crtProps ? Aws::String(crtProps->begin(), crtProps->end()) : "";
                AWS_LOGSTREAM_TRACE(DEFAULT_ENDPOINT_PROVIDER_TAG, "Endpoint rules evaluated props: " << sdkCrtProps);

                Internal::Endpoint::EndpointAttributes epAttributes = Internal::Endpoint::EndpointAttributes::BuildEndpointAttributesFromJson(
                        sdkCrtProps);

                endpoint.SetAttributes(std::move(epAttributes));
            }

            // transform headers
            const auto crtHeaders = resolved->GetHeaders();
            if (crtHeaders)
            {
                Aws::UnorderedMap<Aws::String, Aws::String> sdkHeaders;
                for (const auto& header: *crtHeaders)
                {
                    Aws::String key(header.first.begin(), header.first.end());
                    Aws::String value;
                    for (const auto& crtHeaderValue : header.second)
                    {
                        if(!value.empty()) {
                            value.insert(value.end(), ';');
                        }
                        value.insert(value.end(), crtHeaderValue.begin(), crtHeaderValue.end());
                    }
                    sdkHeaders.emplace(std::move(key), std::move(value));
                }

                endpoint.SetHeaders(std::move(sdkHeaders));
            }

            return ResolveEndpointOutcome(std::move(endpoint));
        }
        else
        {
            return ResolveEndpointOutcome(
                    Aws::Client::AWSError<Aws::Client::CoreErrors>(
                            Aws::Client::CoreErrors::INVALID_QUERY_PARAMETER,
                            "",
                            "Invalid AWS CRT RuleEngine state",
                            false/*retryable*/));
        }
    }

    auto errCode = Aws::Crt::LastError();
    AWS_LOGSTREAM_DEBUG(DEFAULT_ENDPOINT_PROVIDER_TAG, "ERROR: Rule engine has failed to evaluate the endpoint: " << errCode << " " << Aws::Crt::ErrorDebugString(errCode));

    return ResolveEndpointOutcome(
            Aws::Client::AWSError<Aws::Client::CoreErrors>(
                    Aws::Client::CoreErrors::INVALID_QUERY_PARAMETER,
                    "",
                    "Failed to evaluate the endpoint: null output from AWS CRT RuleEngine",
                    false/*retryable*/));

}

} // namespace Endpoint
} // namespace Aws