/*
 * 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 <buildboxcasd_fslocalassetstorage.h>

#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_exception.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_permissions.h>
#include <buildboxcommon_temporaryfile.h>

#include <fstream>
#include <google/protobuf/repeated_field.h>
#include <google/protobuf/util/time_util.h>
#include <string>
#include <unistd.h>

using namespace buildboxcasd;

namespace {
bool expired(const google::protobuf::Timestamp &timestamp)
{
    return google::protobuf::util::TimeUtil::GetCurrentTime() > timestamp;
}

void copyAndSortQualifiers(
    const google::protobuf::RepeatedPtrField<Qualifier> &src,
    google::protobuf::RepeatedPtrField<Qualifier> *dest)
{
    dest->CopyFrom(src);

    // Sort qualifiers by name to avoid cache misses for equivalent requests
    std::sort(dest->begin(), dest->end(),
              [](const Qualifier &a, const Qualifier &b) {
                  return a.name() < b.name();
              });
}
} // namespace

const int FsLocalAssetStorage::s_HASH_PREFIX_LENGTH = 2;

FsLocalAssetStorage::FsLocalAssetStorage(const std::string &root_path)
    : d_storage_root(root_path + ((root_path.back() == '/') ? "" : "/") +
                     "assets"),
      d_objects_directory(d_storage_root + "/objects"),
      d_temp_directory(d_storage_root + "/tmp")

{
    BUILDBOX_LOG_INFO("Creating LocalAssetStorage in " << d_storage_root);

    try {
        buildboxcommon::FileUtils::createDirectory(root_path.c_str());

        buildboxcommon::FileUtils::createDirectory(d_storage_root.c_str());

        buildboxcommon::FileUtils::createDirectory(
            d_objects_directory.c_str());

        buildboxcommon::FileUtils::createDirectory(d_temp_directory.c_str());
    }
    catch (std::system_error &e) {
        BUILDBOX_LOG_ERROR("Could not create asset directory structure in "
                           << d_storage_root << ", " << e.what());
        throw e;
    }
}

std::string FsLocalAssetStorage::filePath(const google::protobuf::Message &key,
                                          bool create_parent_directory)
{
    // Pack as `Any` to include type name in serialization
    google::protobuf::Any any;
    any.PackFrom(key);
    const auto any_serialized = any.SerializeAsString();

    const Digest digest =
        buildboxcommon::DigestGenerator::hash(any_serialized);
    const std::string &hash = digest.hash();
    const std::string directory_name = hash.substr(0, s_HASH_PREFIX_LENGTH);
    const std::string file_name = hash.substr(s_HASH_PREFIX_LENGTH);

    const std::string directory_path =
        d_objects_directory + "/" + directory_name;

    if (create_parent_directory) {
        buildboxcommon::FileUtils::createDirectory(directory_path.c_str());
    }

    return directory_path + "/" + file_name;
}

bool FsLocalAssetStorage::readMessage(const google::protobuf::Message &key,
                                      google::protobuf::Message *value)
{
    const std::string path = filePath(key, false);

    std::ifstream file_stream(path, std::ios::in | std::ios::binary);
    if (!file_stream) {
        return false;
    }

    if (file_stream.peek() == EOF) {
        BUILDBOX_LOG_ERROR("Deleting asset storage entry " << path
                                                           << ": Empty file");
        deleteMessage(key);
        return false;
    }

    // Update timestamp for LRU expiry
    utimes(path.c_str(), nullptr);

    google::protobuf::Any any;
    if (!any.ParseFromIstream(&file_stream)) {
        BUILDBOX_LOG_ERROR("Deleting asset storage entry "
                           << path << ": Failed to parse file");
        deleteMessage(key);
        return false;
    }

    return any.UnpackTo(value);
}

void FsLocalAssetStorage::writeMessage(const google::protobuf::Message &key,
                                       const google::protobuf::Message &value)
{
    const std::string path = filePath(key, true);

    // Pack as `Any` to support cache expiry
    google::protobuf::Any any;
    any.PackFrom(value);

    buildboxcommon::TemporaryFile temp_file(d_temp_directory.c_str(),
                                            "asset-tmpfile", PERMISSION_RWUSR);
    const std::string temp_filename(temp_file.name());

    std::ofstream file(temp_filename, std::fstream::binary);
    any.SerializeToOstream(&file);
    file.close();

    if (!file.good()) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::generic_category,
            "Failed writing to temporary file " + temp_filename);
    }

    if (rename(temp_filename.c_str(), path.c_str()) < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::generic_category,
            "Failed renaming temporary file " + temp_filename + " to " + path);
    }
}

void FsLocalAssetStorage::deleteMessage(const google::protobuf::Message &key)
{
    const std::string path = filePath(key);
    if (unlink(path.c_str()) != 0) {
        if (errno != ENOENT) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::generic_category,
                "Failed deleting asset storage entry: " + path);
        }
    }
}

bool FsLocalAssetStorage::lookup(const FetchBlobRequest &request,
                                 FetchBlobResponse *response)
{
    // Create canonicalized request to determine cache key
    FetchBlobRequest canonicalRequest;

    copyAndSortQualifiers(request.qualifiers(),
                          canonicalRequest.mutable_qualifiers());

    for (const auto &uri : request.uris()) {
        canonicalRequest.clear_uris();
        canonicalRequest.add_uris(uri);

        if (!readMessage(canonicalRequest, response)) {
            continue;
        }

        // Check whether cache entry has expired
        if (response->has_expires_at() && expired(response->expires_at())) {
            response->Clear();
            deleteMessage(canonicalRequest);
            continue;
        }

        return true;
    }

    return false;
}

bool FsLocalAssetStorage::lookup(const FetchDirectoryRequest &request,
                                 FetchDirectoryResponse *response)
{
    // Create canonicalized request to determine cache key
    FetchDirectoryRequest canonicalRequest;

    copyAndSortQualifiers(request.qualifiers(),
                          canonicalRequest.mutable_qualifiers());

    for (const auto &uri : request.uris()) {
        canonicalRequest.clear_uris();
        canonicalRequest.add_uris(uri);

        if (!readMessage(canonicalRequest, response)) {
            continue;
        }

        // Check whether cache entry has expired
        if (response->has_expires_at() && expired(response->expires_at())) {
            response->Clear();
            deleteMessage(canonicalRequest);
            continue;
        }

        return true;
    }

    return false;
}

void FsLocalAssetStorage::insert(const PushBlobRequest &request)
{
    // Create canonicalized request to determine cache key
    FetchBlobRequest canonicalRequest;

    copyAndSortQualifiers(request.qualifiers(),
                          canonicalRequest.mutable_qualifiers());

    // Create fetch response and cache it
    FetchBlobResponse fetchResponse;
    fetchResponse.mutable_qualifiers()->CopyFrom(request.qualifiers());
    fetchResponse.mutable_blob_digest()->CopyFrom(request.blob_digest());

    if (request.has_expire_at()) {
        fetchResponse.mutable_expires_at()->CopyFrom(request.expire_at());
    }

    // Add cache entry for each specified URI
    for (const auto &uri : request.uris()) {
        canonicalRequest.add_uris(uri);
        fetchResponse.set_uri(uri);

        if (!request.has_blob_digest() ||
            (request.has_expire_at() && expired(request.expire_at()))) {
            // Delete entry if no blob is specified or it's already expired
            deleteMessage(canonicalRequest);
        }
        else {
            writeMessage(canonicalRequest, fetchResponse);
        }

        canonicalRequest.clear_uris();
    }
}

void FsLocalAssetStorage::insert(const PushDirectoryRequest &request)
{
    // Create canonicalized request to determine cache key
    FetchDirectoryRequest canonicalRequest;

    copyAndSortQualifiers(request.qualifiers(),
                          canonicalRequest.mutable_qualifiers());

    // Create fetch response and cache it
    FetchDirectoryResponse fetchResponse;
    fetchResponse.mutable_qualifiers()->CopyFrom(request.qualifiers());
    fetchResponse.mutable_root_directory_digest()->CopyFrom(
        request.root_directory_digest());

    if (request.has_expire_at()) {
        fetchResponse.mutable_expires_at()->CopyFrom(request.expire_at());
    }

    // Add cache entry for each specified URI
    for (const auto &uri : request.uris()) {
        canonicalRequest.add_uris(uri);
        fetchResponse.set_uri(uri);

        if (!request.has_root_directory_digest() ||
            (request.has_expire_at() && expired(request.expire_at()))) {
            // Delete entry if no directory is specified or it's already
            // expired
            deleteMessage(canonicalRequest);
        }
        else {
            writeMessage(canonicalRequest, fetchResponse);
        }

        canonicalRequest.clear_uris();
    }
}
