/*
 * Copyright 2022 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 <memory>
#include <signal.h>

#include <buildboxcommon_commandline.h>
#include <buildboxcommon_connectionoptions.h>
#include <buildboxcommon_connectionoptions_commandline.h>
#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_logging_commandline.h>
#include <buildboxcommon_remoteexecutionclient.h>
#include <buildboxcommonmetrics_countingmetricutil.h>
#include <buildboxcommonmetrics_publisherguard.h>
#include <buildboxcommonmetrics_scopedperiodicpublisherdaemon.h>
#include <buildboxcommonmetrics_statsdpublishercreator.h>

#include <trexe_actionbuilder.h>
#include <trexe_cmdlinespec.h>
#include <trexe_executioncontext.h>
#include <trexe_executionoptions.h>
#include <trexe_metricnames.h>

using namespace trexe;
using namespace buildboxcommon;

int cancel(const std::shared_ptr<const ExecutionOptions> &execOptions,
           const ConnectionOptions &connOptions,
           const ConnectionOptions &connOptionsCAS,
           const ConnectionOptions &connOptionsAC)
{
    auto executionContext = ExecutionContext(
        execOptions, connOptionsCAS, connOptions, connOptionsAC, nullptr);

    return executionContext.cancelOperation() ? 0 : 1;
}

int execute(const std::shared_ptr<const ExecutionOptions> &execOptions,
            const ConnectionOptions &connOptions,
            const ConnectionOptions &connOptionsCAS,
            const ConnectionOptions &connOptionsAC,
            const std::shared_ptr<const ConnectionOptions> &connOptionsLS,
            const TrexeMode mode)
{
    // create execution context with provided options
    auto executionContext =
        ExecutionContext(execOptions, connOptionsCAS, connOptions,
                         connOptionsAC, connOptionsLS);

    bool isResultCached = false;
    if (mode == TrexeMode::DOWNLOAD || mode == TrexeMode::WAIT_DOWNLOAD) {
        BUILDBOX_LOG_DEBUG("Checking previously submitted async operation");
    }
    else {
        // Check action cache if applicable
        isResultCached = execOptions->d_argv.size() &&
                         !executionContext.skipsCacheLookup() &&
                         !connOptionsAC.d_url.empty() &&
                         executionContext.isResultCached(true);
        if (isResultCached) {
            BUILDBOX_LOG_DEBUG("Result was cached!");
        }
        else {
            // Submit execution
            // TODO nice error handling if for some reason this didn't work
            BUILDBOX_LOG_DEBUG("Will execute command: " << execOptions.get());
            if (!executionContext.execute(!execOptions->isBlocking())) {
                BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                               "Execution submission failed.");
            }
            BUILDBOX_LOG_DEBUG("Execution service returned...");
        }
    }

    // blocking vs async
    if (mode == TrexeMode::CACHE_ONLY_EXECUTION ||
        mode == TrexeMode::REMOTE_EXECUTION_BLOCKING || isResultCached) {
        std::shared_ptr<ActionResult> ar = executionContext.getActionResult();
        if (execOptions->d_downloadResultsPath.size()) {
            BUILDBOX_LOG_DEBUG(
                "Downloading results from blocking execution...");
            executionContext.downloadResults(
                *ar, execOptions->d_downloadResultsPath);
        }
        // Output stdout/err if they weren't streamed
        if (!execOptions->d_streamLogs ||
            !executionContext.checkLogStreamStatus()) {
            executionContext.outputStdoutStderr();
        }

        int exit_code = ar->exit_code();
        BUILDBOX_LOG_DEBUG("Exit code was: " << exit_code);
        return exit_code;
    } // if async mode, there is nothing to download. The printed OperationId
      // can be used to fetch the status and results later

    if (!execOptions->d_operation.empty()) {
        if (execOptions->d_downloadResultsPath.empty() &&
            execOptions->d_actionResultJsonFile.empty()) {
            BUILDBOX_LOG_ERROR(
                "Ignoring --operation, no download path or action result "
                "filepath specified. Add a --d or --action-result-file "
                "argument");
            return 1;
        }
        else {
            BUILDBOX_LOG_DEBUG(std::string("Downloading ") +
                               execOptions->d_operation + std::string(" to ") +
                               execOptions->d_downloadResultsPath);
            if (!executionContext.downloadCompletedOperation(
                    execOptions->d_downloadResultsPath,
                    mode == TrexeMode::WAIT_DOWNLOAD)) {
                BUILDBOX_LOG_DEBUG("Operation download failed");
                return 1;
            }

            // Output stdout/err of the async command
            executionContext.outputStdoutStderr();

            BUILDBOX_LOG_DEBUG("Operation download succeeded");
            std::shared_ptr<const ActionResult> actionResult =
                executionContext.actionResult();
            if (actionResult == nullptr) {
                BUILDBOXCOMMON_THROW_EXCEPTION(
                    std::logic_error, "ActionResult cannot be null after "
                                      "downloading it successfully");
            }
            BUILDBOX_LOG_DEBUG("Exit code was: " << actionResult->exit_code());
            return actionResult->exit_code();
        }
    }
    return 0;
}

int executeMode(const ExecutionOptions &execOptions)
{
    const TrexeMode mode = execOptions.mode();

    // If --remote is not set, all the urls for the individual channel
    // types (cas/ac/exec) should bet set

    if (mode != TrexeMode::CACHE_ONLY_EXECUTION) {
        if (execOptions.d_casConn.d_url.empty() ||
            execOptions.d_execConn.d_url.empty()) {
            BUILDBOX_LOG_ERROR("Incorrect use of --remote connection options. "
                               "Use default --remote or set a value for "
                               "`cas`, `exec` and optionally `ac` remotes.");
            return 1;
        }
    }
    else if (execOptions.d_casConn.d_url.empty()) {
        BUILDBOX_LOG_ERROR("Incorrect use of --remote connection options. "
                           "Use default --remote or set a value for "
                           "`cas` and optionally `ac` remotes.");
        return 1;
    }

    // TODO: shared_ptr looks unnecessary and abused for our use case?
    if (execOptions.d_cancelMode) {
        return cancel(std::make_shared<const ExecutionOptions>(execOptions),
                      execOptions.d_execConn, execOptions.d_casConn,
                      execOptions.d_acConn);
    }
    else {
        return execute(
            std::make_shared<const ExecutionOptions>(execOptions),
            execOptions.d_execConn, execOptions.d_casConn,
            execOptions.d_acConn,
            std::make_shared<const ConnectionOptions>(execOptions.d_lsConn),
            mode);
    }
}

int main(int argc, char *argv[])
{
    std::vector<std::string> cliArgs(argv, argv + argc);
    // Initialize logger
    buildboxcommon::logging::Logger::getLoggerInstance().initialize(
        cliArgs[0].c_str());

    // Ignore SIGPIPE in case of using sockets + grpc without MSG_NOSIGNAL
    // support configured
    struct sigaction sa{};
    sigemptyset(&sa.sa_mask);
    sa.sa_handler = SIG_IGN;
    sa.sa_flags = 0;
    if (sigaction(SIGPIPE, &sa, nullptr) == -1) {
        BUILDBOX_LOG_ERROR("Unable to ignore SIGPIPE");
        exit(1);
    }

    // Connection Options objects
    CmdLineSpec trexeSpec(
        buildboxcommon::ConnectionOptionsCommandLine("", ""),
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"),
        buildboxcommon::ConnectionOptionsCommandLine("ActionCache", "ac-"),
        buildboxcommon::ConnectionOptionsCommandLine("Execution", "exec-"),
        buildboxcommon::ConnectionOptionsCommandLine("LogStream",
                                                     "logstream-"));

    // Comandline parsing
    CommandLine commandLine(trexeSpec.d_spec);
    const bool success = commandLine.parse(argc, argv);
    if (!success || argc == 1) {
        commandLine.usage();
        return 1;
    }
    if (commandLine.exists("help") || commandLine.exists("version")) {
        return 0;
    }

    const ExecutionOptions execOptions =
        ExecutionOptions::fromCommandLine(trexeSpec.d_command, commandLine);

    execOptions.validate();
    BUILDBOX_LOG_SET_LEVEL(execOptions.d_logLevel);

    buildboxcommon::DigestGenerator::init(execOptions.d_digestFunction);

    BUILDBOX_LOG_DEBUG("Exec connection: " << execOptions.d_execConn);
    BUILDBOX_LOG_DEBUG("CAS connection: " << execOptions.d_casConn);
    BUILDBOX_LOG_DEBUG("AC connection: " << execOptions.d_acConn);
    BUILDBOX_LOG_DEBUG("Logstream connection: " << execOptions.d_lsConn);

    // Setup Metrics
    buildboxcommon::buildboxcommonmetrics::MetricsConfigType metricsConfig;
    if (execOptions.d_metricMode != "") {
        buildboxcommonmetrics::MetricsConfigurator::metricsParser(
            "metrics-mode", execOptions.d_metricMode, &metricsConfig);
        buildboxcommonmetrics::MetricsConfigurator::metricsParser(
            "metrics-publish-interval",
            std::to_string(execOptions.d_metricPublishInterval),
            &metricsConfig);
        buildboxcommonmetrics::MetricsConfigurator::metricsParser(
            "metrics-prefix", execOptions.d_metricPrefix, &metricsConfig);
    }

    std::shared_ptr<buildboxcommon::buildboxcommonmetrics::StatsDPublisherType>
        metricsPublisher = buildboxcommon::buildboxcommonmetrics::
            MetricsConfigurator::createMetricsPublisherWithConfig<
                buildboxcommon::buildboxcommonmetrics::StatsDPublisherType>(
                metricsConfig);

    buildboxcommonmetrics::ScopedPeriodicPublisherDaemon<
        buildboxcommonmetrics::StatsDPublisherType>
        statsDPublisherGuard(metricsConfig.enable(), metricsConfig.interval(),
                             *metricsPublisher.get());

    // Execution
    try {
        const int exitCode = executeMode(execOptions);
        if (exitCode == 0) {
            buildboxcommonmetrics::CountingMetricUtil::recordCounterMetric(
                metricnames::SUCCESS, 1);
        }
        return exitCode;
    }
    catch (const std::exception &e) {
        BUILDBOX_LOG_ERROR("Error in trexe: " << e.what());
        buildboxcommonmetrics::CountingMetricUtil::recordCounterMetric(
            metricnames::RAISED_EXCEPTION, 1);
        return 1;
    }

    return 0;
}
