/*
 * Copyright 2019 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_casinstance.h>
#include <buildboxcasd_metricnames.h>

#include <buildboxcommon_casclient.h>
#include <buildboxcommon_exception.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommonmetrics_countingmetricutil.h>
#include <buildboxcommonmetrics_distributionmetricutil.h>

#include <fstream>

using namespace buildboxcasd;
using namespace buildboxcommon;

CasInstance::CasInstance(const std::string &instance_name)
    : d_instance_name(instance_name)
{
    d_root_dirfd =
        FileDescriptor(open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
    if (d_root_dirfd.get() < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(std::system_error, errno,
                                              std::system_category,
                                              "Could not open root directory");
    }
}
CasInstance::~CasInstance() {}

CasInstance::CasInstance(const CasInstance &other,
                         const std::string &instanceName,
                         const std::string &root)
    : d_instance_name(instanceName), d_chrooted(true)
{
    int newrootFd = -1;

    if (!other.d_chrooted) {
        // The old root is the root of buildbox-casd's mount namespace, so
        // there is no need to use `openInRoot()`. Use regular `openat()` here
        // to allow `root` to be a 'magic link' such as `/proc/PID/root` on
        // Linux, which `openInRoot()` can't safely handle.
        newrootFd = openat(other.rootDirfd(), root.c_str(),
                           O_RDONLY | O_DIRECTORY | O_CLOEXEC);
    }
    else {
        // The path to the new root is relative to the old root to prevent
        // escaping from the old root.
        newrootFd = FileUtils::openInRoot(other.rootDirfd(), root,
                                          O_RDONLY | O_DIRECTORY | O_CLOEXEC);
    }

    if (newrootFd < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Could not open directory " + root);
    }
    d_root_dirfd = FileDescriptor(newrootFd);
}

grpc::Status CasInstance::Read(const ReadRequest &request,
                               ServerWriter<ReadResponse> *writer,
                               Digest *requested_digest)
{
    const std::string &resource_name = request.resource_name();

    BUILDBOX_LOG_DEBUG("Bytestream.Read(" << resource_name << ")");

    Digest digest;
    grpc::Status argument_status =
        bytestreamReadArgumentStatus(request, &digest);

    if (!argument_status.ok()) {
        return argument_status;
    }

    *requested_digest = digest;

    const auto total_read_limit = request.read_limit();
    /* According to the REAPI specification:
     * "The maximum number of `data` bytes the server is allowed to return in
     * the sum of all `ReadResponse` messages. A `read_limit` of zero
     * indicates that there is no limit, and a negative `read_limit` will
     * cause an error."
     */

    const auto blob_size = digest.size_bytes();
    auto read_offset = request.read_offset();

    google::protobuf::int64 bytes_remaining =
        (total_read_limit == 0) ? blob_size - read_offset : total_read_limit;

    do {
        const auto chunk_size = std::min(
            static_cast<size_t>(bytes_remaining),
            static_cast<size_t>(
                buildboxcommon::CASClient::bytestreamChunkSizeBytes()));
        // We use the same limit defined by `buildboxcommon::Client` for the
        // number of bytes that can be transferred inside a single gRPC
        // message.

        std::string data;
        data.reserve(chunk_size);

        const auto status = readBlob(
            digest, &data, static_cast<size_t>(read_offset), chunk_size);

        if (status.code() != grpc::StatusCode::OK) {
            return grpc::Status(static_cast<grpc::StatusCode>(status.code()),
                                status.message());
        }

        ReadResponse response;
        response.set_data(data);
        writer->Write(response);

        read_offset += static_cast<int64_t>(data.size());
        bytes_remaining -= static_cast<int64_t>(data.size());
    } while (bytes_remaining > 0 && read_offset < blob_size);

    return grpc::Status::OK;
}

grpc::Status CasInstance::processWriteRequest(
    WriteRequest *request_message, ServerReader<WriteRequest> &request,
    Digest *digest, const std::string &buffer_path)
{
    const std::string resource_name = request_message->resource_name();

    BUILDBOX_LOG_DEBUG("Bytestream.Write(" << resource_name << ")");

    try {
        *digest = digestFromUploadResourceName(resource_name, d_instance_name);
    }
    catch (const std::invalid_argument &) {
        return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
                            "Invalid resource name.");
    }

    // According to the REAPI spec.:
    // "When attempting an upload, if another client has already completed the
    // upload (which may occur in the middle of a single upload if another
    // client uploads the same blob concurrently), the request will terminate
    // immediately [...]".
    if (hasBlob(*digest)) {
        return grpc::Status(grpc::StatusCode::ALREADY_EXISTS, "");
    }

    auto digest_context =
        buildboxcommon::DigestGenerator::createDigestContext();

    bool commit_write = false;

    std::ofstream output_file(buffer_path, std::ofstream::binary);

    google::protobuf::int64 bytes_written = 0;
    do {
        const std::string &new_request_resource_name =
            request_message->resource_name();
        // If the `resource_name` was set on subsequent requests, it has to
        // match the initial resource name. However, `resource_name` is
        // optional and can be omitted in subsequent requests.
        // ref:
        // https://github.com/googleapis/googleapis/blob/6da3d64919c006ef40cad2026f1e39084253afe2/google/bytestream/bytestream.proto#L130-133
        if (!new_request_resource_name.empty() &&
            new_request_resource_name != resource_name) {
            return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
                                "`resource_name` changed between requests.");
        }
        else if (commit_write) {
            // `finish_write` must be set only in the last request.
            return grpc::Status(
                grpc::StatusCode::INVALID_ARGUMENT,
                "Extra request sent after setting `finish_write`.");
        }

        if (request_message->write_offset() != bytes_written) {
            // (Only serial writes are allowed)
            grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
                         "`write_offset` is not valid");
        }

        // All arguments are valid, appending to the buffer:
        const auto &data = request_message->data();
        output_file << data;
        digest_context.update(data.c_str(), data.size());
        bytes_written += static_cast<int64_t>(data.size());

        commit_write = request_message->finish_write();
    } while (request.Read(request_message));

    // Finished receiving requests.

    if (!commit_write) {
        return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
                            "`finish_write` was not set in the last request.");
    }

    if (bytes_written != digest->size_bytes()) {
        std::ostringstream error_message;
        error_message << "Data size does not match digest size. "
                      << "(Digest reported " << digest->size_bytes()
                      << " but received " << bytes_written << " bytes)";

        BUILDBOX_LOG_DEBUG(error_message.str());
        return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
                            error_message.str());
    }

    const auto received_digest = digest_context.finalizeDigest();
    if (received_digest != *digest) {
        std::ostringstream error_message;
        error_message << "Expected blob with digest " << *digest
                      << ", but received blob has digest " << received_digest;

        BUILDBOX_LOG_DEBUG(error_message.str());
        return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
                            error_message.str());
    }

    return grpc::Status::OK;
}

grpc::Status CasInstance::parsePageToken(const std::string &page_token,
                                         int *starting_page_number)
{
    // If a token is not specified, we start at the beginning.
    if (page_token.empty()) {
        *starting_page_number = 0;
        return grpc::Status::OK;
    }

    std::string error_reason;
    try {
        const int page_token_value = std::stoi(page_token);
        if (page_token_value > 0) {
            *starting_page_number = page_token_value;
            return grpc::Status::OK;
        }

        error_reason = "cannot be non-positive.";
    }
    catch (const std::invalid_argument &) {
        error_reason = "failed to parse it as a valid integer.";
    }
    catch (const std::out_of_range &) {
        error_reason = "out of range.";
    }

    return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
                        "Invalid `page_token` value: " + error_reason);
}

grpc::Status CasInstance::GetTree(const GetTreeRequest &request,
                                  ServerWriter<GetTreeResponse> *writer)
{
    const Digest &root_digest = request.root_digest();
    BUILDBOX_LOG_DEBUG("GetTree(" << root_digest.hash() << ")");

    // `page_size`: maximum number of entries to be returned.
    // If set to a value larger than 0, return at most that many items.
    // Otherwise return all the entries in the tree, making sure that it is
    // split across `GetTreeResponse`s that fit inside gRPC messages.
    if (request.page_size() < 0) {
        return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
                            "`page_size` cannot be negative.");
    }

    // `page_token`: "If present, the server will use that token as an offset,
    // returning only that page and the ones that succeed it."
    int starting_page = 0;
    auto page_token_status =
        parsePageToken(request.page_token(), &starting_page);
    if (!page_token_status.ok()) {
        BUILDBOX_LOG_DEBUG("Error while getting tree rooted at \""
                           << request.root_digest()
                           << "\": " << page_token_status.error_message());
        return page_token_status;
    }

    try {
        const Directory root_directory = getDirectory(root_digest);
        auto get_tree_status = getTree(root_directory, request.page_size(),
                                       starting_page, writer);

        if (!get_tree_status.ok()) {
            BUILDBOX_LOG_DEBUG("Error while getting tree rooted at \""
                               << request.root_digest()
                               << "\": " << get_tree_status.error_message());
        }

        return get_tree_status;
    }
    catch (const BlobNotFoundException &) {
        return grpc::Status(grpc::StatusCode::NOT_FOUND,
                            "The root digest was not found in the local CAS.");
    }
    catch (const std::runtime_error &e) {
        const std::string error_message =
            "GetTree(): Error building tree rooted at \"" +
            toString(root_digest) + "\": " + e.what();
        BUILDBOX_LOG_ERROR(error_message);
        return grpc::Status(grpc::StatusCode::INTERNAL, error_message);
    }
}

Directory CasInstance::getDirectory(const Digest &digest)
{
    std::string subdirectory_blob;

    const google::rpc::Status read_status =
        readBlob(digest, &subdirectory_blob, 0, 0);

    if (read_status.code() == grpc::StatusCode::OK) {
        Directory d;
        if (d.ParseFromString(subdirectory_blob)) {
            return d;
        }
        else {
            BUILDBOX_LOG_DEBUG(digest.hash() << "/" << digest.size_bytes()
                                             << ": error parsing Directory.");
        }
    }
    BUILDBOXCOMMON_THROW_EXCEPTION(
        BlobNotFoundException,
        "Directory blob could not be read from local CAS");
}

grpc::Status CasInstance::getTree(const Directory &directory,
                                  const int page_max_items,
                                  const int starting_page_number,
                                  ServerWriter<GetTreeResponse> *writer)
{
    GetTreeResponse current_page;
    int current_page_number = 0;
    size_t current_page_size = 0;

    writeTree(directory, page_max_items, starting_page_number, &current_page,
              &current_page_number, &current_page_size, writer);

    if (current_page_number < starting_page_number) {
        // The tree ended before reaching the requested page number.
        return grpc::Status(
            grpc::StatusCode::INVALID_ARGUMENT,
            "Invalid `page_token`: could not find specified page in tree.");
    }

    writer->Write(current_page);
    return grpc::Status::OK;
}

void CasInstance::writeTree(const Directory &directory,
                            const int page_max_items,
                            const int starting_page_number,
                            GetTreeResponse *current_page,
                            int *current_page_number,
                            size_t *current_page_size,
                            ServerWriter<GetTreeResponse> *writer)
{
    static const auto max_grpc_payload_size =
        buildboxcommon::GrpcClient::maxMessageSizeBytes();

    const auto directory_byte_size = directory.ByteSizeLong();
    if (directory_byte_size > max_grpc_payload_size) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "Directory proto larger than gRPC maximum payload size ("
                << std::to_string(directory_byte_size) << " > "
                << std::to_string(max_grpc_payload_size) << " bytes)");
    }

    const bool item_limit_reached =
        (page_max_items != 0) &&
        (current_page->directories_size() >= page_max_items);

    const bool size_limit_reached =
        (*current_page_size + directory_byte_size > max_grpc_payload_size);

    if (item_limit_reached || size_limit_reached) {
        // The current page is full. We flush the current one if necessary,
        // and start a new one.

        if (*current_page_number >= starting_page_number) {
            // We only stream pages beginning from `starting_page_number`.
            const auto next_page_token =
                std::to_string(*current_page_number + 1);
            current_page->set_next_page_token(next_page_token);

            writer->Write(*current_page);
        }

        current_page->Clear();
        *current_page_number += 1;
        *current_page_size = 0;
    }

    // Adding the directory to the response:
    Directory *directory_entry = current_page->add_directories();
    directory_entry->CopyFrom(directory);
    *current_page_size += directory_byte_size;

    // And recursively adding all its subdirectories to the response...
    for (const DirectoryNode &subdirectory_node : directory.directories()) {
        // (sub)DirectoryNode -> directory_node (blob) -> Directory proto
        const Digest &subdirectory_digest = subdirectory_node.digest();
        try {
            const Directory subdirectory_proto =
                getDirectory(subdirectory_digest);
            writeTree(subdirectory_proto, page_max_items, starting_page_number,
                      current_page, current_page_number, current_page_size,
                      writer);
        }
        catch (const BlobNotFoundException &) {
            ;
        }
        // (According to the RE specification, if some portion of the tree
        // is missing from the CAS, we still have to return what we have. So we
        // just skip missing subdirectories.)
    }
}

/*
 * Helpers to parse resource name URLs and extract digests
 */
Digest
CasInstance::digestFromDownloadResourceName(const std::string &resource_name,
                                            const std::string &instance_name)
{
    /* Checks that the resource name has the form:
     * "{instance_name}/blobs/{hash}/{size}",
     * and attempts to parse the hash and size and return a Digest object.
     * If not, raise an invalid argument exception.
     */
    std::string prefix_string = instance_name;
    if (!prefix_string.empty()) {
        prefix_string.append("/blobs/");
    }
    else {
        prefix_string.append("blobs/");
    }
    const std::size_t blobs_pos = resource_name.find(prefix_string);
    if (blobs_pos != 0) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::invalid_argument, "Resource name \"" << resource_name << "\" "
                                                      << "is not valid");
    }
    const std::size_t hash_start = blobs_pos + prefix_string.size();
    return digestFromPartialResourceName(resource_name, hash_start);
}

Digest
CasInstance::digestFromUploadResourceName(const std::string &resource_name,
                                          const std::string &instance_name)
{
    /* Checks that the resource name has the form:
     * "{instance_name}/uploads/{uuid}/blobs/{hash}/{size}{/optional_metadata}",
     * and attempts to parse the hash and size and return a Digest object.
     * If not, raise an invalid argument exception.
     */

    std::string prefix_string = instance_name;
    if (!prefix_string.empty()) {
        prefix_string.append("/uploads/");
    }
    else {
        prefix_string.append("uploads/");
    }
    const std::size_t uploads_pos = resource_name.find(prefix_string);
    if (uploads_pos != 0) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::invalid_argument, "Resource name \"" << resource_name << "\" "
                                                      << "is not valid");
    }
    const std::string blobs_segment = "blobs/";
    const std::size_t blobs_pos =
        resource_name.find(blobs_segment, prefix_string.size());
    if (blobs_pos == std::string::npos) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::invalid_argument, "Resource name \"" << resource_name << "\" "
                                                      << "is not valid");
    }
    const std::size_t hash_start = blobs_pos + blobs_segment.size();
    return digestFromPartialResourceName(resource_name, hash_start);
}

Digest
CasInstance::digestFromPartialResourceName(const std::string &resource_name,
                                           const std::size_t &hash_start)
{
    const std::size_t hash_end = resource_name.find('/', hash_start);
    if (hash_end == std::string::npos) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::invalid_argument, "Resource name \"" << resource_name << "\" "
                                                      << "is not valid");
    }
    const std::size_t size_start = hash_end + 1;
    const std::string resource_hash =
        resource_name.substr(hash_start, hash_end - hash_start);
    // There may be trailing optional metadata, so only grab up to the next
    // '/' for the size, or the end of the string if there's no remaining '/'
    const std::size_t size_end = resource_name.find('/', size_start);
    const std::string size_bytes =
        resource_name.substr(size_start, size_end - size_start);
    std::size_t parsed_size_pos = 0;
    const auto parsed_size = std::stoll(size_bytes, &parsed_size_pos);
    if (parsed_size < 0 || parsed_size_pos != size_bytes.length()) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::invalid_argument,
                                       "Parsed digest size \""
                                           << size_bytes << "\" "
                                           << "is not valid");
    }
    Digest d;
    d.set_hash(resource_hash);
    d.set_size_bytes(parsed_size);
    return d;
}

grpc::Status
CasInstance::bytestreamReadArgumentStatus(const ReadRequest &request,
                                          Digest *digest)
{
    const auto read_limit = request.read_limit();
    if (read_limit < 0) {
        return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
                            "`read_limit` cannot be negative.");
    }

    const auto read_offset = request.read_offset();
    if (read_offset < 0) {
        return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
                            "`read_offset` cannot be negative.");
    }

    // Checking that the resource name of the request is pertinent and if
    // so extracting the Digest value from it:
    const std::string &resource_name = request.resource_name();
    try {
        const Digest requested_digest =
            digestFromDownloadResourceName(resource_name, d_instance_name);

        if (read_offset > requested_digest.size_bytes()) {
            return grpc::Status(
                grpc::StatusCode::OUT_OF_RANGE,
                "`read_offset` cannot be larger than the size of the "
                "data: " +
                    std::to_string(read_offset) + " > " +
                    std::to_string(requested_digest.size_bytes()));
        }

        digest->CopyFrom(requested_digest);
        return grpc::Status::OK;
    }
    catch (const std::invalid_argument &) {
        return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
                            "The requested resource (\"" + resource_name +
                                "\") is not valid.");
    }
}

Status CasInstance::FetchMissingBlobs(const FetchMissingBlobsRequest &,
                                      FetchMissingBlobsResponse *)
{
    return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, __func__);
}

Status CasInstance::UploadMissingBlobs(const UploadMissingBlobsRequest &,
                                       UploadMissingBlobsResponse *)
{
    return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, __func__);
}

Status CasInstance::FetchTree(const FetchTreeRequest &, FetchTreeResponse *)
{
    return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, __func__);
}

Status CasInstance::UploadTree(const UploadTreeRequest &, UploadTreeResponse *)
{
    return grpc::Status(grpc::StatusCode::UNIMPLEMENTED, __func__);
}

FetchTreeCache *CasInstance::getTreeCache() const
{
    return &this->d_tree_cache;
}

void CasInstance::recordTreeCacheMetrics(int64_t cache_hits,
                                         int64_t cache_misses)
{
    if (cache_hits + cache_misses == 0) {
        BUILDBOX_LOG_WARNING("Failed to collect tree cache metrics");
    }
    else {
        buildboxcommonmetrics::CountingMetricUtil::recordCounterMetric(
            MetricNames::COUNTER_NAME_LOCAL_CAS_TREE_CACHE_HITS, cache_hits);
        buildboxcommonmetrics::CountingMetricUtil::recordCounterMetric(
            MetricNames::COUNTER_NAME_LOCAL_CAS_TREE_CACHE_MISSES,
            cache_misses);
        constexpr int PERCENTAGE = 100;
        buildboxcommon::buildboxcommonmetrics::DistributionMetricUtil::
            recordDistributionMetric(
                MetricNames::
                    DISTRIBUTION_NAME_LOCAL_CAS_TREE_CACHE_HIT_PERCENTAGE,
                cache_hits * PERCENTAGE / (cache_hits + cache_misses));
    }
}

int CasInstance::openInRoot(const std::string &root, const std::string &path,
                            int flags)
{
    FileDescriptor clientRootDirfd(
        FileUtils::openInRoot(rootDirfd(), root.empty() ? "/" : root,
                              O_RDONLY | O_DIRECTORY | O_CLOEXEC));
    return FileUtils::openInRoot(clientRootDirfd.get(), path, flags);
}
