/*
 * 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 <buildboxcasd_digestcache.h>
#include <buildboxcasd_localacproxyinstance.h>
#include <buildboxcasd_metricnames.h>
#include <buildboxcasd_requestcontextmanager.h>

#include <buildboxcommon_grpcclient.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_protos.h>

#include <buildboxcommonmetrics_countingmetricutil.h>
#include <buildboxcommonmetrics_countingmetricvalue.h>
#include <buildboxcommonmetrics_durationmetrictimer.h>
#include <buildboxcommonmetrics_metricguard.h>

#include <optional>
#include <set>
#include <string>

using namespace buildboxcasd;
using namespace buildboxcommon;

LocalAcProxyInstance::LocalAcProxyInstance(
    const std::shared_ptr<ActionStorage> &storage,
    const std::shared_ptr<CasInstance> &cas, bool readOnly,
    const buildboxcommon::ConnectionOptions &ac_endpoint,
    const bool read_only_remote, const int validationCacheTtl)
    : AcInstance(storage, cas, readOnly),
      d_remote_instance_name(ac_endpoint.d_instanceName),
      d_read_only_remote(read_only_remote),
      d_validationCache(
          validationCacheTtl > 0
              ? std::make_optional<DigestCache>(validationCacheTtl)
              : std::nullopt)
{
    std::shared_ptr<buildboxcommon::GrpcClient> grpcClient =
        std::make_shared<buildboxcommon::GrpcClient>();
    grpcClient->init(ac_endpoint);
    RequestContextManager::configureGrpcClient(grpcClient.get());

    d_ac_client = std::make_shared<buildboxcommon::RemoteExecutionClient>(
        nullptr, grpcClient);
    d_ac_client->init();

    /* readOnly is intended to deny update requests from clients.
     * The local cache may still be updated, though.
     */
    d_local_instance =
        std::make_shared<LocalAcInstance>(storage, cas, false /* readOnly */);
}

std::shared_ptr<AcInstance>
LocalAcProxyInstance::clone(std::shared_ptr<CasInstance> cas, bool readOnly)
{
    return std::make_shared<LocalAcProxyInstance>(*this, cas, readOnly);
}

grpc::Status
LocalAcProxyInstance::GetActionResult(const GetActionResultRequest &request,
                                      ActionResult *result)
{

    buildboxcommon::buildboxcommonmetrics::MetricGuard<
        buildboxcommon::buildboxcommonmetrics::DurationMetricTimer>
        mt(MetricNames::TIMER_NAME_AC_GET_ACTION_RESULT);

    BUILDBOX_LOG_INFO("Checking ActionCache for result of ActionDigest='" +
                      buildboxcommon::toString(request.action_digest()) + "'");
    bool read = d_storage->readAction(request.action_digest(), result);
    if (!read) {
        BUILDBOX_LOG_INFO("Checking proxy ActionCache for ActionDigest='" +
                          buildboxcommon::toString(request.action_digest()) +
                          "'");

        std::set<std::string> outputs;
        for (const std::string &s : request.inline_output_files()) {
            outputs.insert(s);
        }
        if (!d_ac_client->fetchFromActionCache(request.action_digest(),
                                               outputs, result)) {

            buildboxcommonmetrics::CountingMetricUtil::recordCounterMetric(
                MetricNames::COUNTER_NAME_AC_GET_ACTION_RESULT_MISSES, 1);

            BUILDBOX_LOG_INFO(
                "Some output blob(s) missing from cache for ActionDigest='" +
                buildboxcommon::toString(request.action_digest()) + "'");

            return grpc::Status(grpc::StatusCode::NOT_FOUND,
                                "Not found in ActionCache");
        }
        buildboxcommonmetrics::CountingMetricUtil::recordCounterMetric(
            MetricNames::COUNTER_NAME_AC_GET_ACTION_RESULT_REMOTE_HITS, 1);
        // Update local cache to reduce later request count
        UpdateActionResultRequest uar_req;
        uar_req.set_instance_name(request.instance_name());
        uar_req.mutable_action_digest()->CopyFrom(request.action_digest());
        uar_req.mutable_action_result()->CopyFrom(*result);

        if (!d_local_instance->UpdateActionResult(uar_req, result).ok()) {
            BUILDBOX_LOG_INFO(
                "Local ActionCache not updated to reflect remote. Write "
                "failed for ActionDigest='" +
                buildboxcommon::toString(request.action_digest()) + "'");
        }
    }

    if (d_validationCache &&
        d_validationCache->hasDigest(request.action_digest())) {
        BUILDBOX_LOG_DEBUG(
            "ActionResult found in validation cache for ActionDigest='"
            << buildboxcommon::toString(request.action_digest()) << "'");
        buildboxcommonmetrics::CountingMetricUtil::recordCounterMetric(
            MetricNames::COUNTER_NAME_AC_VALIDATION_CACHE_HITS, 1);
        return grpc::Status::OK;
    }
    else {
        buildboxcommonmetrics::CountingMetricUtil::recordCounterMetric(
            MetricNames::COUNTER_NAME_AC_VALIDATION_CACHE_MISSES, 1);
    }

    if (AcInstance::hasAllDigests(result)) {
        BUILDBOX_LOG_INFO("ActionResult found for ActionDigest='" +
                          buildboxcommon::toString(request.action_digest()) +
                          "'");

        buildboxcommonmetrics::CountingMetricUtil::recordCounterMetric(
            MetricNames::COUNTER_NAME_AC_GET_ACTION_RESULT_HITS, 1);

        // Add the ActionDigest to the validation cache so the validation can
        // be skipped in near future
        if (d_validationCache) {
            d_validationCache->addDigest(request.action_digest());
        }

        return grpc::Status::OK;
    }
    else {
        buildboxcommonmetrics::CountingMetricUtil::recordCounterMetric(
            MetricNames::COUNTER_NAME_AC_GET_ACTION_RESULT_OUTPUT_MISSES, 1);

        BUILDBOX_LOG_INFO("ActionResult not found in CAS for ActionDigest='" +
                          buildboxcommon::toString(request.action_digest()) +
                          "'");
        return grpc::Status(grpc::StatusCode::NOT_FOUND,
                            "ActionResult's output files not found in CAS");
    }
}

grpc::Status LocalAcProxyInstance::UpdateActionResult(
    const UpdateActionResultRequest &request, ActionResult *result)
{
    if (isReadOnly()) {
        return grpc::Status(grpc::StatusCode::PERMISSION_DENIED,
                            "Write access to action cache denied");
    }

    // Write to upstream ActionCache then to local cache, in this order.
    if (!d_read_only_remote) {
        BUILDBOX_LOG_INFO(
            "Updating ActionResult to upstream server for ActionDigest='" +
            buildboxcommon::toString(request.action_digest()) + "'");
        d_ac_client->updateActionCache(request.action_digest(),
                                       request.action_result());
    }

    BUILDBOX_LOG_INFO("Updating ActionResult to local server (this proxy) for "
                      "ActionDigest='" +
                      buildboxcommon::toString(request.action_digest()) + "'");
    auto localStatus = d_local_instance->UpdateActionResult(request, result);
    if (localStatus.ok() && d_validationCache) {
        d_validationCache->addDigest(request.action_digest());
    }
    return localStatus;
}
