/*
 * Copyright 2021-2026 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_fslocalactionstorage.h>
#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_exception.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_fslocalcas.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_systemutils.h>
#include <buildboxcommon_temporaryfile.h>

#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <stdio.h>

using namespace buildboxcasd;
using namespace buildboxcommon;

const int FsLocalActionStorage::s_HASH_PREFIX_LENGTH = 2;

static void renameTemp(const std::string &final_path,
                       const std::string &temp_path)
{
    const int result = rename(temp_path.c_str(), final_path.c_str());
    if (result != 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Could not rename temporary file " << temp_path << " to "
                                               << final_path);
    }
}

FsLocalActionStorage::FsLocalActionStorage(const std::string &rootPath)
    : d_storageRoot(rootPath + ((rootPath.back() == '/') ? "" : "/") +
                    "actioncache"),
      d_objectsDirectory(d_storageRoot + "/objects"),
      d_tempDirectory(d_storageRoot + "/tmp")
{
    BUILDBOX_LOG_INFO("ActionCache storage in " << d_storageRoot);

    bool migrate =
        buildboxcommon::FileUtils::isDirectory(d_storageRoot.c_str()) &&
        !buildboxcommon::FileUtils::isDirectory(d_objectsDirectory.c_str());

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

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

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

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

    if (migrate) {
        // Migrate actions to the new directory structure
        try {
            const auto hashLength =
                buildboxcommon::DigestGenerator::hash("").hash().size();

            for (DirentWrapper dir(d_storageRoot); dir.entry() != nullptr;
                 dir.next()) {
                std::string filename =
                    FileUtils::pathBasename(dir.currentEntryPath());
                if (dir.currentEntryIsFile() &&
                    filename.size() == hashLength &&
                    std::all_of(filename.cbegin(), filename.cend(),
                                [](char c) { return std::isxdigit(c); })) {
                    Digest digest;
                    digest.set_hash(filename);
                    const std::string newPath = filePath(digest, true);
                    renameTemp(newPath, dir.currentEntryPath());
                }
            }
        }
        catch (std::system_error &e) {
            BUILDBOX_LOG_ERROR(
                "Failed to migrate action to new directory structure: "
                << e.what());
            throw e;
        }
    }
}

std::string FsLocalActionStorage::filePath(const Digest &actionDigest,
                                           bool createParentDirectory)
{
    const std::string &hash = actionDigest.hash();
    const std::string directoryName = hash.substr(0, s_HASH_PREFIX_LENGTH);
    const std::string fileName = hash.substr(s_HASH_PREFIX_LENGTH);

    const std::string directoryPath = d_objectsDirectory + "/" + directoryName;

    if (createParentDirectory) {
        buildboxcommon::FileUtils::createDirectory(directoryPath.c_str());
    }

    return directoryPath + "/" + fileName;
}

void FsLocalActionStorage::storeAction(const Digest &action_digest,
                                       const ActionResult &result)
{
    const std::string final_filename = filePath(action_digest, true);

    buildboxcommon::TemporaryFile temp_file(d_tempDirectory.c_str(),
                                            "actioncache-tmpfile");
    const std::string temp_filename(temp_file.name());

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

    if (!file.good()) {
        const std::string error_message = "Failed writing to temporary file " +
                                          temp_filename + ": " +
                                          strerror(errno);
        BUILDBOX_LOG_ERROR(error_message);

        if (errno == ENOSPC) {
            BUILDBOXCOMMON_THROW_EXCEPTION(OutOfSpaceException, error_message);
        }
        else {
            BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error, error_message);
        }
    }

    renameTemp(final_filename, temp_filename);
}

bool FsLocalActionStorage::readAction(const Digest &action_digest,
                                      ActionResult *result)
{
    const std::string path = filePath(action_digest, false);

    std::ifstream action_file(path, std::fstream::binary);
    if (action_file.fail()) {
        return false;
    }

    if (action_file.peek() == EOF) {
        BUILDBOX_LOG_ERROR("Deleting action result for " << action_digest
                                                         << ": Empty file");
        unlink(path.c_str());
        return false;
    }

    if (!result->ParseFromIstream(&action_file)) {
        BUILDBOX_LOG_ERROR("Deleting action result for "
                           << action_digest << ": Failed to parse file");
        unlink(path.c_str());
        return false;
    }

    return true;
}
