/*
 * Copyright 2018 Bloomberg Finance LP
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <buildboxcommon_connectionoptions.h>
#include <buildboxcommon_credentialhelperauthenticator.h>
#include <buildboxcommon_exception.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_reloadtokenauthenticator.h>
#include <buildboxcommon_stringutils.h>

#include <cerrno>
#include <cstring>
#include <grpcpp/client_context.h>
#include <grpcpp/create_channel.h>
#include <grpcpp/security/credentials.h>
#include <iomanip>
#include <iostream>
#include <stdexcept>

namespace buildboxcommon {

namespace {
constexpr const char *HTTP_PREFIX = "http://";
constexpr const char *HTTPS_PREFIX = "https://";
constexpr const char *GRPC_PREFIX = "grpc://";
constexpr const char *GRPCS_PREFIX = "grpcs://";
constexpr const char *UNIX_SOCKET_PREFIX = "unix:";

void printPadded(int padWidth, const std::string &str)
{
    char previousFill = std::cerr.fill(' ');
    std::ios_base::fmtflags previousFlags(std::cerr.flags());
    std::cerr << "    ";
    constexpr int PADDING = 5;
    std::cerr << std::left << std::setw(padWidth - PADDING) << str;
    std::cerr << " ";
    std::cerr.flags(previousFlags);
    std::cerr.fill(previousFill);
}

bool starts_with(const std::string &s1, const char *s2)
{
    return s1.substr(0, strlen(s2)) == s2;
}

} // namespace

void ConnectionOptions::setClientCert(const std::string &value)
{
    this->d_clientCert = value;
}

void ConnectionOptions::setServerCertPath(const std::string &value)
{
    this->d_serverCertPath = value;
}

void ConnectionOptions::setClientKey(const std::string &value)
{
    this->d_clientKey = value;
}

void ConnectionOptions::setClientKeyPath(const std::string &value)
{
    this->d_clientKeyPath = value;
}

void ConnectionOptions::setAccessTokenPath(const std::string &value)
{
    this->d_accessTokenPath = value;
}

void ConnectionOptions::setTokenReloadInterval(const std::string &value)
{
    this->d_tokenReloadInterval = value;
}

void ConnectionOptions::setInstanceName(const std::string &value)
{
    this->d_instanceName = value;
}

void ConnectionOptions::setRetryDelay(const std::string &value)
{
    this->d_retryDelay = value;
}

void ConnectionOptions::setRetryLimit(const std::string &value)
{
    this->d_retryLimit = value;
}

void ConnectionOptions::setRetryOnCodes(
    const std::set<grpc::StatusCode> &value)
{
    this->d_retryOnCodes = value;
}

void ConnectionOptions::setRequestTimeout(const std::string &value)
{
    this->d_requestTimeout = value;
}

void ConnectionOptions::setMinThroughput(const std::string &value)
{
    this->d_minThroughput = StringUtils::parseSize(value);
}

void ConnectionOptions::setKeepaliveTime(const std::string &value)
{
    this->d_keepaliveTime = value;
}

void ConnectionOptions::setServerCert(const std::string &value)
{
    this->d_serverCert = value;
}

void ConnectionOptions::setClientCertPath(const std::string &value)
{
    this->d_clientCertPath = value;
}

void ConnectionOptions::setUrl(const std::string &value)
{
    this->d_url = value;
}

void ConnectionOptions::setUseGoogleApiAuth(const bool value)
{
    this->d_useGoogleApiAuth = value;
}

void ConnectionOptions::setLoadBalancingPolicy(const std::string &value)
{
    this->d_loadBalancingPolicy = value;
}

bool ConnectionOptions::parseArg(std::string_view argView,
                                 std::optional<std::string_view> prefix)
{
    if (argView.size() < 2 || argView.substr(0, 2) != "--") {
        return false;
    }
    argView.remove_prefix(2); // Remove the "--"
    if (prefix.has_value()) {
        std::string_view prefixView = prefix.value();
        if (argView.size() < prefixView.size() ||
            argView.substr(0, prefixView.size()) != prefixView) {
            return false;
        }
        argView.remove_prefix(prefixView.size()); // Remove the prefix
    }
    const size_t assignPos = argView.find('=');
    if (assignPos != std::string_view::npos) {
        const std::string_view key = argView.substr(0, assignPos);
        const std::string value(argView.substr(assignPos + 1));
        if (key == "remote") {
            this->d_url = value;
            return true;
        }
        else if (key == "instance") {
            this->d_instanceName = value;
            return true;
        }
        else if (key == "server-cert") {
            this->d_serverCertPath = value;
            return true;
        }
        else if (key == "client-key") {
            this->d_clientKeyPath = value;
            return true;
        }
        else if (key == "client-cert") {
            this->d_clientCertPath = value;
            return true;
        }
        else if (key == "access-token") {
            this->d_accessTokenPath = value;
            return true;
        }
        else if (key == "retry-limit") {
            this->d_retryLimit = value;
            return true;
        }
        else if (key == "retry-delay") {
            this->d_retryDelay = value;
            return true;
        }
        else if (key == "request-timeout") {
            this->d_requestTimeout = value;
            return true;
        }
        else if (key == "min-throughput") {
            this->setMinThroughput(value);
            return true;
        }
        else if (key == "keepalive-time") {
            this->d_keepaliveTime = value;
            return true;
        }
        else if (key == "token-reload-interval") {
            this->d_tokenReloadInterval = value;
            return true;
        }
        else if (key == "load-balancing-policy") {
            this->d_loadBalancingPolicy = value;
            return true;
        }
        else if (key == "credential-helper") {
            this->d_credentialHelperPath = value;
            return true;
        }
    }
    else if (argView == "googleapi-auth") {
        this->d_useGoogleApiAuth = true;
        return true;
    }
    return false;
}

void ConnectionOptions::putArgs(std::vector<std::string> *out,
                                const char *prefix) const
{
    const std::string p(prefix == nullptr ? "" : prefix);
    if (!this->d_url.empty()) {
        out->push_back("--" + p + "remote=" + this->d_url);
    }
    if (!this->d_instanceName.empty()) {
        out->push_back("--" + p + "instance=" + this->d_instanceName);
    }
    if (!this->d_serverCertPath.empty()) {
        out->push_back("--" + p + "server-cert=" + this->d_serverCertPath);
    }
    if (!this->d_clientKeyPath.empty()) {
        out->push_back("--" + p + "client-key=" + this->d_clientKeyPath);
    }
    if (!this->d_clientCertPath.empty()) {
        out->push_back("--" + p + "client-cert=" + this->d_clientCertPath);
    }
    if (!this->d_accessTokenPath.empty()) {
        out->push_back("--" + p + "access-token=" + this->d_accessTokenPath);
    }
    if (!this->d_tokenReloadInterval.empty()) {
        out->push_back("--" + p +
                       "token-reload-interval=" + this->d_tokenReloadInterval);
    }
    if (!this->d_retryLimit.empty()) {
        out->push_back("--" + p + "retry-limit=" + this->d_retryLimit);
    }
    if (!this->d_retryDelay.empty()) {
        out->push_back("--" + p + "retry-delay=" + this->d_retryDelay);
    }
    if (!this->d_requestTimeout.empty()) {
        out->push_back("--" + p + "request-timeout=" + this->d_requestTimeout);
    }
    if (this->d_minThroughput > 0) {
        out->push_back("--" + p + "min-throughput=" +
                       std::to_string(this->d_minThroughput));
    }
    if (!this->d_keepaliveTime.empty()) {
        out->push_back("--" + p + "keepalive-time=" + this->d_keepaliveTime);
    }
    if (this->d_useGoogleApiAuth) {
        out->emplace_back("--googleapi-auth");
    }
    if (!this->d_loadBalancingPolicy.empty()) {
        out->push_back("--" + p +
                       "load-balancing-policy=" + this->d_loadBalancingPolicy);
    }
}

std::shared_ptr<grpc::Channel> ConnectionOptions::createChannel() const
{
    BUILDBOX_LOG_DEBUG("Creating grpc channel to [" << this->d_url << "]");
    std::string target;
    std::shared_ptr<grpc::ChannelCredentials> creds;
    grpc::ChannelArguments channel_args;
    bool secure = false;

    if (starts_with(this->d_url, HTTP_PREFIX)) {
        target = this->d_url.substr(strlen(HTTP_PREFIX));
    }
    else if (starts_with(this->d_url, GRPC_PREFIX)) {
        target = this->d_url.substr(strlen(GRPC_PREFIX));
    }
    else if (starts_with(this->d_url, HTTPS_PREFIX)) {
        target = this->d_url.substr(strlen(HTTPS_PREFIX));
        secure = true;
    }
    else if (starts_with(this->d_url, GRPCS_PREFIX)) {
        target = this->d_url.substr(strlen(GRPCS_PREFIX));
        secure = true;
    }
    else if (starts_with(this->d_url, UNIX_SOCKET_PREFIX)) {
        target = this->d_url;
    }
    else {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                       "Unsupported URL scheme");
    }
    if (secure) {
        auto options = grpc::SslCredentialsOptions();
        if (!this->d_serverCert.empty()) {
            options.pem_root_certs = this->d_serverCert;
        }
        else if (!this->d_serverCertPath.empty()) {
            options.pem_root_certs =
                FileUtils::getFileContents(this->d_serverCertPath.c_str());
        }
        if (!this->d_clientKey.empty()) {
            options.pem_private_key = this->d_clientKey;
        }
        else if (!this->d_clientKeyPath.empty()) {
            options.pem_private_key =
                FileUtils::getFileContents(this->d_clientKeyPath.c_str());
        }
        if (!this->d_clientCert.empty()) {
            options.pem_cert_chain = this->d_clientCert;
        }
        else if (!this->d_clientCertPath.empty()) {
            options.pem_cert_chain =
                FileUtils::getFileContents(this->d_clientCertPath.c_str());
        }

        // Check for mutually exclusive authentication methods
        int authMethodCount = 0;
        if (!this->d_accessTokenPath.empty())
            authMethodCount++;
        if (this->d_useGoogleApiAuth)
            authMethodCount++;
        if (!this->d_credentialHelperPath.empty())
            authMethodCount++;

        if (authMethodCount > 1) {
            BUILDBOXCOMMON_THROW_EXCEPTION(
                std::runtime_error,
                "Cannot use multiple authentication methods (access token, "
                "GoogleAPI auth, and credential helper are mutually "
                "exclusive).");
        }

        if (this->d_useGoogleApiAuth) {
            creds = grpc::GoogleDefaultCredentials();
            if (!creds) {
                throw std::runtime_error(
                    "Failed to initialize GoogleAPIAuth. Make Sure you have a "
                    "token and have set the appropriate environment variable "
                    "[GOOGLE_APPLICATION_CREDENTIALS] as necessary.");
            }
        }
        else {
            creds = grpc::SslCredentials(options);

            std::shared_ptr<grpc::CallCredentials> call_creds;
            if (!this->d_accessTokenPath.empty()) {
                call_creds = grpc::MetadataCredentialsFromPlugin(
                    std::make_unique<ReloadTokenAuthenticator>(
                        this->d_accessTokenPath,
                        this->d_tokenReloadInterval.empty()
                            ? nullptr
                            : this->d_tokenReloadInterval.c_str()));
            }
            else if (!this->d_credentialHelperPath.empty()) {
                call_creds = grpc::MetadataCredentialsFromPlugin(
                    std::make_unique<CredentialHelperAuthenticator>(
                        this->d_credentialHelperPath));
            }

            if (call_creds) {
                creds = grpc::CompositeChannelCredentials(creds, call_creds);
            }
        }
    }
    else {
        creds = grpc::InsecureChannelCredentials();
        // If any of the secure-only options were specified with an insecure
        // endpoint, throw
        if (!this->d_serverCert.empty() || !this->d_serverCertPath.empty() ||
            !this->d_clientKey.empty() || !this->d_clientKeyPath.empty() ||
            !this->d_clientCert.empty() || !this->d_clientCertPath.empty() ||
            !this->d_accessTokenPath.empty() || this->d_useGoogleApiAuth ||
            !this->d_credentialHelperPath.empty()) {
            BUILDBOXCOMMON_THROW_EXCEPTION(
                std::runtime_error,
                "Secure Channel options cannot be used with this URL");
        }
    }

    if (!this->d_loadBalancingPolicy.empty()) {
        channel_args.SetLoadBalancingPolicyName(this->d_loadBalancingPolicy);
    }

    if (this->d_keepaliveTime != "0") {
        const int keepaliveTime = std::stoi(this->d_keepaliveTime);
        const int KEEPALIVE_TIME_MS = 1000;
        channel_args.SetInt(GRPC_ARG_KEEPALIVE_TIME_MS,
                            keepaliveTime * KEEPALIVE_TIME_MS);
    }

    return grpc::CreateCustomChannel(target, creds, channel_args);
}

void ConnectionOptions::printArgHelp(int padWidth, const char *serviceName,
                                     const char *prefix)
{
    std::string p(prefix == nullptr ? "" : prefix);

    printPadded(padWidth, "--" + p + "remote=URL");
    std::clog << "URL for " << serviceName << " service\n";

    printPadded(padWidth, "--" + p + "instance=NAME");
    std::clog << "Name of the " << serviceName << " instance\n";

    printPadded(padWidth, "--" + p + "server-cert=PATH");
    std::clog << "Public server certificate for TLS (PEM-encoded)\n";

    printPadded(padWidth, "--" + p + "client-key=PATH");
    std::clog << "Private client key for TLS (PEM-encoded)\n";

    printPadded(padWidth, "--" + p + "client-cert=PATH");
    std::clog << "Public client certificate for TLS (PEM-encoded)\n";

    printPadded(padWidth, "--" + p + "access-token=PATH");
    std::clog << "Access Token for authentication "
                 "(e.g. JWT, OAuth access token, etc), "
                 "will be included as an HTTP Authorization bearer token.\n";

    printPadded(padWidth, "--" + p + "token-reload-interval=MINUTES");
    std::clog
        << "Time to wait before refreshing access token from disk again. "
           "The following suffixes can be optionally specified: "
           "M (minutes), H (hours). "
           "Value defaults to minutes if suffix not specified.\n";

    printPadded(padWidth, "--" + p + "googleapi-auth");
    std::clog << "Use GoogleAPIAuth when this flag is set.\n";

    printPadded(padWidth, "--" + p + "credential-helper=PATH");
    std::clog
        << "Path to credential helper executable for dynamic authentication.\n"
        << "                            Example: "
           "--credential-helper=/usr/bin/helper\n";

    printPadded(padWidth, "--" + p + "retry-limit=INT");
    std::clog << "Number of times to retry on grpc errors\n";

    printPadded(padWidth, "--" + p + "retry-delay=MILLISECONDS");
    std::clog << "How long to wait before the first grpc retry\n";

    printPadded(padWidth, "--" + p + "retry-on-code");
    std::clog << "Which gRPC status code(s) to retry on\n";

    printPadded(padWidth, "--" + p + "request-timeout=SECONDS");
    std::clog << "How long to wait for gRPC request responses\n";

    printPadded(padWidth, "--" + p + "min-throughput=BPS");
    std::clog << "Minimum throughput in bytes per second. "
                 "The value may be suffixed with K, M, G or T.\n";

    printPadded(padWidth, "--" + p + "keepalive-time=SECONDS");
    std::clog << "The period after which a keepalive ping is sent\n";

    printPadded(padWidth, "--" + p + "load-balancing-policy");
    std::clog << "Which grpc load balancing policy to use. "
                 "Valid options are 'round_robin' and 'grpclb'\n";
}

std::ostream &operator<<(std::ostream &out, const ConnectionOptions &obj)
{
    std::ostringstream retryOnCodes;
    retryOnCodes << "[";
    bool first = true;
    for (const auto &code : obj.d_retryOnCodes) {
        if (!first) {
            retryOnCodes << ",";
        }
        retryOnCodes << static_cast<int>(code);
        first = false;
    }
    retryOnCodes << "]";

    out << "url = \"" << obj.d_url << "\", instance = \"" << obj.d_instanceName
        << "\", serverCert = \"" << obj.d_serverCert
        << "\", serverCertPath = \"" << obj.d_serverCertPath
        << "\", clientKey = \"" << obj.d_clientKey << "\", clientKeyPath = \""
        << obj.d_clientKeyPath << "\", clientCert = \"" << obj.d_clientCert
        << "\", clientCertPath = \"" << obj.d_clientCertPath
        << "\", accessTokenPath = \"" << obj.d_accessTokenPath
        << "\", token-reload-interval = \"" << obj.d_tokenReloadInterval
        << "\", googleapi-auth = " << std::boolalpha << obj.d_useGoogleApiAuth
        << ", retry-limit = \"" << obj.d_retryLimit << "\", retry-delay = \""
        << obj.d_retryDelay << "\", retry-on-code = \"" << retryOnCodes.str()
        << "\", request-timeout = \"" << obj.d_requestTimeout
        << "\", min-throughput = \"" << obj.d_minThroughput
        << "\", keepalive-time = \"" << obj.d_keepaliveTime
        << "\", load-balancing-policy = \"" << obj.d_loadBalancingPolicy
        << "\"";

    return out;
}

void ConnectionOptions::reset()
{
    d_url = "";
    d_instanceName = "";
    d_serverCertPath = "";
    d_clientKeyPath = "";
    d_clientCertPath = "";
    d_accessTokenPath = "";
    d_tokenReloadInterval = "";
    d_useGoogleApiAuth = false;
    d_credentialHelperPath = "";
    d_retryLimit = "4";
    d_retryDelay = "1000";
    d_retryOnCodes.clear();
    d_requestTimeout = "0";
    d_minThroughput = 0;
    d_keepaliveTime = "0";
    d_loadBalancingPolicy = "";
}

} // namespace buildboxcommon
