//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012- OpenVPN Inc.
//
//    SPDX-License-Identifier: MPL-2.0 OR AGPL-3.0-only WITH openvpn3-openssl-exception
//

// AWS REST API query utilities such as query signing

#pragma once

#include <string>
#include <vector>
#include <cstdint> // for std::uint8_t
#include <algorithm>
#include <utility>
#include <time.h>

#include <openvpn/common/exception.hpp>
#include <openvpn/common/hexstr.hpp>
#include <openvpn/http/urlencode.hpp>
#include <openvpn/crypto/digestapi.hpp>
#include <openvpn/aws/awscreds.hpp>

namespace openvpn::AWS {
class REST
{
  public:
    OPENVPN_EXCEPTION(aws_rest_error);

    // 20130524T000000Z
    static std::string amz_date()
    {
        struct tm lt;
        char buf[64];
        const time_t t = ::time(nullptr);
        if (!::gmtime_r(&t, &lt))
            throw aws_rest_error("gmtime_r failed");
        if (!::strftime(buf, sizeof(buf), "%Y%m%dT%H%M%SZ", &lt))
            throw aws_rest_error("strftime failed");
        return std::string(buf);
    }

    struct SHA256
    {
        std::string to_hex() const
        {
            return render_hex(hash, sizeof(hash));
        }

        std::uint8_t hash[32];
    };

    static SHA256 hmac_sha256(DigestFactory &digest_factory, const std::string &data, const std::string &key)
    {
        SHA256 ret;
        HMACInstance::Ptr hi(digest_factory.new_hmac(CryptoAlgs::SHA256, (const std::uint8_t *)key.c_str(), key.length()));
        hi->update((const std::uint8_t *)data.c_str(), data.length());
        hi->final(ret.hash);
        return ret;
    }

    static SHA256 hmac_sha256(DigestFactory &digest_factory, const std::string &data, const SHA256 &key)
    {
        SHA256 ret;
        HMACInstance::Ptr hi(digest_factory.new_hmac(CryptoAlgs::SHA256, key.hash, sizeof(key.hash)));
        hi->update((const std::uint8_t *)data.c_str(), data.length());
        hi->final(ret.hash);
        return ret;
    }

    static SHA256 sha256(DigestFactory &digest_factory, const std::string &data)
    {
        SHA256 ret;
        DigestInstance::Ptr di(digest_factory.new_digest(CryptoAlgs::SHA256));
        di->update((const std::uint8_t *)data.c_str(), data.length());
        di->final(ret.hash);
        return ret;
    }

    static SHA256 signing_key(DigestFactory &df,
                              const std::string &key,
                              const std::string &date_stamp,
                              const std::string &region_name,
                              const std::string &service_name)
    {
        const SHA256 h1 = hmac_sha256(df, date_stamp, "AWS4" + key);
        const SHA256 h2 = hmac_sha256(df, region_name, h1);
        const SHA256 h3 = hmac_sha256(df, service_name, h2);
        const SHA256 h4 = hmac_sha256(df, "aws4_request", h3);
        return h4;
    }

    struct KeyValue
    {
        KeyValue(std::string key_arg, std::string value_arg)
            : key(std::move(key_arg)),
              value(std::move(value_arg))
        {
        }

        bool operator<(const KeyValue &rhs) const
        {
            return key < rhs.key;
        }

        std::string uri_encode() const
        {
            return URL::encode(key) + '=' + URL::encode(value);
        }

        std::string key;
        std::string value;
    };

    struct Query : public std::vector<KeyValue>
    {
        std::string canonical_query_string() const
        {
            bool first = true;
            std::string ret;
            for (auto &p : *this)
            {
                if (!first)
                    ret += '&';
                ret += p.uri_encode();
                first = false;
            }
            return ret;
        }

        void sort()
        {
            std::sort(begin(), end());
        }
    };

    struct QueryBuilder
    {
        std::string date;           // such as "20130524T000000Z"
        unsigned int expires = 300; // request expiration in seconds
        std::string region;         // such as "us-east-1"
        std::string service;        // such as "s3"
        std::string method;         // such as "GET"
        std::string host;           // such as "ec2.us-west-2.amazonaws.com"
        std::string uri;            // such as "/"
        Query parms;

        std::string uri_query() const
        {
            return uri + '?' + parms.canonical_query_string();
        }

        std::string url_query() const
        {
            return "https://" + host + uri_query();
        }

        void add_amz_parms(const Creds &creds)
        {
            parms.emplace_back("X-Amz-Algorithm", "AWS4-HMAC-SHA256");
            parms.emplace_back("X-Amz-Credential", creds.access_key + '/' + amz_credential());
            parms.emplace_back("X-Amz-Date", date);
            parms.emplace_back("X-Amz-Expires", std::to_string(expires));
            parms.emplace_back("X-Amz-SignedHeaders", amz_signed_headers());

            if (!creds.token.empty())
                parms.emplace_back("X-Amz-Security-Token", creds.token);
        }

        void sort_parms()
        {
            parms.sort();
        }

        void add_amz_signature(DigestFactory &digest_factory, const Creds &creds)
        {
            parms.emplace_back("X-Amz-Signature", signature(digest_factory, creds));
        }

        std::string signature(DigestFactory &digest_factory, const Creds &creds) const
        {
            const SHA256 sk = signing_key(digest_factory,
                                          creds.secret_key,
                                          date.substr(0, 8),
                                          region,
                                          service);
            return hmac_sha256(digest_factory, string_to_sign(digest_factory), sk).to_hex();
        }

        virtual std::string content_hash() const
        {
            // SHA256 of empty string
            return "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
        }

        std::string canonical_request() const
        {
            std::string ret = method + '\n'
                              + uri + '\n'
                              + parms.canonical_query_string() + '\n'
                              + "host:" + host + '\n'
                              + '\n'
                              + amz_signed_headers() + '\n';
            if (service == "s3")
                ret += "UNSIGNED-PAYLOAD";
            else
                ret += content_hash();
            return ret;
        }

        std::string amz_signed_headers() const
        {
            std::string signed_headers = "host";
            return signed_headers;
        }

        std::string string_to_sign(DigestFactory &digest_factory) const
        {
            return "AWS4-HMAC-SHA256\n"
                   + date + '\n'
                   + amz_credential() + "\n"
                   + sha256(digest_factory, canonical_request()).to_hex();
        }

        std::string amz_credential() const
        {
            return date.substr(0, 8) + '/' + region + '/' + service + "/aws4_request";
        }

        virtual ~QueryBuilder() = default;
    };
};
} // namespace openvpn::AWS
