/*
 * Copyright 2020 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_commandline.h>

#include <buildboxcommon_connectionoptions.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_stringutils.h>

namespace buildboxcommon {

using ArgumentSpec = buildboxcommon::CommandLineTypes::ArgumentSpec;
using DataType = buildboxcommon::CommandLineTypes::DataType;
using TypeInfo = buildboxcommon::CommandLineTypes::TypeInfo;
using DefaultValue = buildboxcommon::CommandLineTypes::DefaultValue;

ConnectionOptionsCommandLine::ConnectionOptionsCommandLine(
    const std::string &serviceName, const std::string &commandLinePrefix)
{
    auto addOption = [&](const std::string &name,
                         const std::string &defaultServiceHelp,
                         const std::string &serviceHelp,
                         CommandLineTypes::TypeInfo optType) {
        d_spec.emplace_back(
            commandLinePrefix + name,
            (serviceName.empty() ? defaultServiceHelp : serviceHelp), optType,
            ArgumentSpec::O_OPTIONAL, ArgumentSpec::C_WITH_ARG);
    };

    addOption("remote", "URL for all services",
              "URL for the " + serviceName + " service",
              TypeInfo(DataType::COMMANDLINE_DT_STRING));
    addOption("instance", "Instance for all services",
              "Name of the " + serviceName + " instance",
              TypeInfo(DataType::COMMANDLINE_DT_STRING));
    addOption("server-cert",
              "Server TLS certificate for all services (PEM-encoded)",
              "Server TLS certificate for " + serviceName + " (PEM-encoded)",
              TypeInfo(DataType::COMMANDLINE_DT_STRING));
    addOption("client-key",
              "Client private TLS key far all services (PEM-encoded)",
              "Client private TLS key for " + serviceName + " (PEM-encoded)",
              TypeInfo(DataType::COMMANDLINE_DT_STRING));
    addOption("client-cert",
              "Client TLS certificate for all services (PEM-encoded)",
              "Client TLS certificate for " + serviceName + " (PEM-encoded)",
              TypeInfo(DataType::COMMANDLINE_DT_STRING));
    std::string accessTokenSuffix =
        " (JWT, OAuth token etc), will be included as an "
        "HTTP  Authorization bearer token";
    addOption("access-token",
              "Authentication token for all services" + accessTokenSuffix,
              "Authentication token for " + serviceName + accessTokenSuffix,
              TypeInfo(DataType::COMMANDLINE_DT_STRING));
    addOption("token-reload-interval", "Default access token refresh timeout",
              "Access token refresh timeout for " + serviceName + " service",
              TypeInfo(DataType::COMMANDLINE_DT_STRING));
    addOption("googleapi-auth", "Use GoogleAPIAuth for all services",
              "Use GoogleAPIAuth for " + serviceName + " service",
              TypeInfo(DataType::COMMANDLINE_DT_BOOL));
    addOption("credential-helper",
              "Credential helper executable for all services",
              "Credential helper executable for " + serviceName + " service",
              TypeInfo(DataType::COMMANDLINE_DT_STRING));
    addOption("retry-limit", "Retry limit for gRPC errors for all services",
              "Retry limit for gRPC errors for " + serviceName + " service",
              TypeInfo(DataType::COMMANDLINE_DT_STRING));
    addOption("retry-delay", "Retry delay for gRPC errors for all services",
              "Retry delay for gRPC errors for " + serviceName + " service",
              TypeInfo(DataType::COMMANDLINE_DT_STRING));
    addOption("retry-on-code",
              "gRPC status code(s) as string(s) to retry on for all services, "
              "e.g. 'UNKNOWN', 'INTERNAL'",
              "gRPC status code(s) as string(s) to retry on for " +
                  serviceName + " service e.g., 'UNKNOWN', 'INTERNAL'",
              TypeInfo(DataType::COMMANDLINE_DT_STRING_ARRAY));
    std::string requestTimeoutSuffix = " (set to 0 to disable timeout)";
    addOption("request-timeout",
              "Timeout for gRPC requests for all services " +
                  requestTimeoutSuffix,
              "Timeout for gRPC requests for " + serviceName + " service" +
                  requestTimeoutSuffix,
              TypeInfo(DataType::COMMANDLINE_DT_STRING));
    std::string minThroughputSuffix =
        ", bytes per seconds. The value may be suffixed with K, M, G or T.";
    addOption("min-throughput",
              "Minimum throughput for gRPC requests for all services" +
                  minThroughputSuffix,
              "Minimum throughput for gRPC requests for " + serviceName +
                  " service" + minThroughputSuffix,
              TypeInfo(DataType::COMMANDLINE_DT_STRING));
    std::string keepaliveTimeSuffix = " (set to 0 to disable keepalive pings)";
    addOption("keepalive-time",
              "gRPC keepalive pings period for all services" +
                  keepaliveTimeSuffix,
              "gRPC keepalive pings period for " + serviceName + " service" +
                  keepaliveTimeSuffix,
              TypeInfo(DataType::COMMANDLINE_DT_STRING));
    std::string loadBalancingPolicySuffix =
        " (valid options are 'round_robin' and 'grpclb')";
    addOption("load-balancing-policy",
              "gRPC load balancing policy for all services" +
                  loadBalancingPolicySuffix,
              "gRPC load balancing policy for " + serviceName + " service" +
                  loadBalancingPolicySuffix,
              TypeInfo(DataType::COMMANDLINE_DT_STRING));
}

bool ConnectionOptionsCommandLine::configureChannel(
    const CommandLine &cml, const std::string &commandLinePrefix,
    ConnectionOptions *channel)
{
    if (channel == nullptr) {
        BUILDBOX_LOG_ERROR("invalid argument: 'channel' is nullptr");
        return false;
    }

    resetChannelOptions(channel);
    updateChannelOptions(cml, commandLinePrefix, channel);

    return true;
}

bool ConnectionOptionsCommandLine::updateChannelOptions(
    const CommandLine &cml, const std::string &commandLinePrefix,
    ConnectionOptions *channel)
{
    if (channel == nullptr) {
        BUILDBOX_LOG_ERROR("invalid argument: 'channel' is nullptr");
        return false;
    }

    auto optRemote = commandLinePrefix + "remote";
    auto hasRemote = cml.exists(optRemote);

    auto optInstance = commandLinePrefix + "instance";
    auto hasInstance = cml.exists(optInstance);

    auto optServerCert = commandLinePrefix + "server-cert";
    auto hasServerCert = cml.exists(optServerCert);

    auto optClientKey = commandLinePrefix + "client-key";
    auto hasClientKey = cml.exists(optClientKey);

    auto optClientCert = commandLinePrefix + "client-cert";
    auto hasClientCert = cml.exists(optClientCert);

    auto optAccessToken = commandLinePrefix + "access-token";
    auto hasAccessToken = cml.exists(optAccessToken);

    auto optTokenReloadInterval = commandLinePrefix + "token-reload-interval";
    auto hasTokenReloadInterval = cml.exists(optTokenReloadInterval);

    auto optGoogleAPIAuth = commandLinePrefix + "googleapi-auth";
    auto hasGoogleAPIAuth = cml.exists(optGoogleAPIAuth);

    auto optCredentialHelper = commandLinePrefix + "credential-helper";
    auto hasCredentialHelper = cml.exists(optCredentialHelper);

    auto optRetryLimit = commandLinePrefix + "retry-limit";
    auto hasRetryLimit = cml.exists(optRetryLimit);

    auto optRetryDelay = commandLinePrefix + "retry-delay";
    auto hasRetryDelay = cml.exists(optRetryDelay);

    // It's a vector but buildbox uses singular option names by convention
    auto optRetryOnCodes = commandLinePrefix + "retry-on-code";
    auto hasRetryOnCodes = cml.exists(optRetryOnCodes);

    auto optRequestTimeout = commandLinePrefix + "request-timeout";
    auto hasRequestTimeout = cml.exists(optRequestTimeout);

    auto optMinThroughput = commandLinePrefix + "min-throughput";
    auto hasMinThroughput = cml.exists(optMinThroughput);

    auto optKeepaliveTime = commandLinePrefix + "keepalive-time";
    auto hasKeepaliveTime = cml.exists(optKeepaliveTime);

    auto optLBPolicy = commandLinePrefix + "load-balancing-policy";
    auto hasLBPolicy = cml.exists(optLBPolicy);

    // Check the option compatibility

    bool failed = false;

    if (hasClientKey && !hasClientCert) {
        BUILDBOX_LOG_ERROR("invalid options: --" + optClientCert +
                           " is required with --" + optClientKey);
        failed = true;
    }
    if (hasClientCert && !hasClientKey) {
        BUILDBOX_LOG_ERROR("invalid options: --" + optClientKey +
                           " is required with --" + optClientCert);
        failed = true;
    }

    // Some --*-foo options make sense only in conjunction with --*-remote
    if (commandLinePrefix != "" && !hasRemote) {
        if (hasServerCert) {
            BUILDBOX_LOG_ERROR("invalid options: --" + optRemote +
                               " is required with --" + optServerCert);
            failed = true;
        }
        if (hasClientCert) {
            BUILDBOX_LOG_ERROR("invalid options: --" + optRemote +
                               " is required with --" + optClientCert);
            failed = true;
        }
        if (hasClientKey) {
            BUILDBOX_LOG_ERROR("invalid options: --" + optRemote +
                               " is required with --" + optClientKey);
            failed = true;
        }
        if (hasAccessToken) {
            BUILDBOX_LOG_ERROR("invalid options: --" + optRemote +
                               " is required with --" + optAccessToken);
            failed = true;
        }
        if (hasGoogleAPIAuth) {
            BUILDBOX_LOG_ERROR("invalid options: --" + optRemote +
                               " is required with --" + optGoogleAPIAuth);
            failed = true;
        }
        if (hasCredentialHelper) {
            BUILDBOX_LOG_ERROR("invalid options: --" + optRemote +
                               " is required with --" + optCredentialHelper);
            failed = true;
        }
    }

    if (failed) {
        return false;
    }

    // Set the options, their combinations is valid

    if (hasRemote)
        channel->d_url = cml.getString(optRemote);

    if (hasInstance)
        channel->d_instanceName = cml.getString(optInstance);

    if (hasServerCert)
        channel->d_serverCertPath = cml.getString(optServerCert);

    if (hasClientKey)
        channel->d_clientKeyPath = cml.getString(optClientKey);

    if (hasClientCert)
        channel->d_clientCertPath = cml.getString(optClientCert);

    if (hasAccessToken)
        channel->d_accessTokenPath = cml.getString(optAccessToken);

    if (hasTokenReloadInterval)
        channel->d_tokenReloadInterval = cml.getString(optTokenReloadInterval);

    if (hasGoogleAPIAuth)
        channel->d_useGoogleApiAuth = cml.getBool(optGoogleAPIAuth);

    if (hasCredentialHelper)
        channel->d_credentialHelperPath = cml.getString(optCredentialHelper);

    if (hasRetryLimit)
        channel->d_retryLimit = cml.getString(optRetryLimit);

    if (hasRetryDelay)
        channel->d_retryDelay = cml.getString(optRetryDelay);

    if (hasRetryOnCodes) {
        const auto &strCodes = cml.getVS(optRetryOnCodes);
        channel->d_retryOnCodes.clear();
        for (const auto &strCode : strCodes) {
            channel->d_retryOnCodes.emplace(
                StringUtils::parseStatusCode(strCode));
        }
    }

    if (hasRequestTimeout)
        channel->d_requestTimeout = cml.getString(optRequestTimeout);

    if (hasMinThroughput)
        channel->setMinThroughput(cml.getString(optMinThroughput));

    if (hasKeepaliveTime)
        channel->d_keepaliveTime = cml.getString(optKeepaliveTime);

    if (hasLBPolicy)
        channel->d_loadBalancingPolicy = cml.getString(optLBPolicy);

    return true;
}

bool ConnectionOptionsCommandLine::resetChannelOptions(
    ConnectionOptions *channel)
{
    channel->reset();
    return true;
}
} // namespace buildboxcommon
