// Copyright 2018 Bloomberg Finance L.P
//
// 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 <fileutils.h>

#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_logging.h>

#include <cstring>
#include <env.h>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <unistd.h>

namespace recc {

bool FileUtils::isRegularFileOrSymlink(const struct stat &s)
{
    return (S_ISREG(s.st_mode) || S_ISLNK(s.st_mode));
}

struct stat FileUtils::getStat(const std::string &path,
                               const bool followSymlinks)
{
    if (path.empty()) {
        const std::string error = "invalid args: path empty";
        BUILDBOX_LOG_ERROR(error);
        throw std::runtime_error(error);
    }

    struct stat statResult{};
    const int rc = (followSymlinks ? stat(path.c_str(), &statResult)
                                   : lstat(path.c_str(), &statResult));
    if (rc < 0) {
        BUILDBOX_LOG_ERROR("Error calling "
                           << (followSymlinks ? "stat()" : "lstat()")
                           << " for path \"" << path << "\": "
                           << "rc = " << rc << ", errno = [" << errno << ":"
                           << strerror(errno) << "]");
        throw std::system_error(errno, std::system_category());
    }

    return statResult;
}

bool FileUtils::isExecutable(const struct stat &s)
{
    return s.st_mode & S_IXUSR;
}

bool FileUtils::isSymlink(const struct stat &s) { return S_ISLNK(s.st_mode); }

std::string FileUtils::getSymlinkContents(const std::string &path,
                                          const struct stat &statResult)
{
    if (path.empty()) {
        const std::string error = "invalid args: path is empty";
        BUILDBOX_LOG_ERROR(error);
        throw std::runtime_error(error);
    }

    if (!S_ISLNK(statResult.st_mode)) {
        std::ostringstream oss;
        oss << "file \"" << path << "\" is not a symlink";
        BUILDBOX_LOG_ERROR(oss.str());
        throw std::runtime_error(oss.str());
    }

    std::string contents(static_cast<size_t>(statResult.st_size), '\0');
    const ssize_t rc = readlink(path.c_str(), &contents[0], contents.size());
    if (rc < 0) {
        std::ostringstream oss;
        oss << "readlink failed for \"" << path << "\", rc = " << rc
            << ", errno = [" << errno << ":" << strerror(errno) << "]";
        BUILDBOX_LOG_ERROR(oss.str());
        throw std::runtime_error(oss.str());
    }

    return contents;
}

bool FileUtils::hasPathPrefix(const std::string &path,
                              const std::string &prefix)
{
    /* A path can never have the empty path as a prefix */
    if (prefix.empty()) {
        return false;
    }
    if (path == prefix) {
        return true;
    }

    /*
     * Make sure prefix ends in a slash.
     * This is so we don't return true if path = /foo and prefix = /foobar
     */
    std::string tmpPrefix(prefix);
    if (tmpPrefix.back() != '/') {
        tmpPrefix.push_back('/');
    }

    return path.substr(0, tmpPrefix.length()) == tmpPrefix;
}

bool FileUtils::hasPathPrefixes(const std::string &path,
                                const std::set<std::string> &pathPrefixes)
{
    for (const auto &prefix : pathPrefixes) {
        if (FileUtils::hasPathPrefix(path, prefix)) {
            return true;
        }
    }

    return false;
}

std::string FileUtils::getCurrentWorkingDirectory()
{
    const unsigned int BUFFER_SIZE = 1024;
    unsigned int bufferSize = BUFFER_SIZE;
    while (true) {
        std::unique_ptr<char[]> buffer(new char[bufferSize]);
        char *cwd = getcwd(buffer.get(), bufferSize);

        if (cwd != nullptr) {
            return std::string(cwd);
        }
        else if (errno == ERANGE) {
            bufferSize *= 2;
        }
        else {
            const std::string errorReason = strerror(errno);
            BUILDBOX_LOG_ERROR(
                "Warning: could not get current working directory: "
                << errorReason);
            return std::string();
        }
    }
}

int FileUtils::parentDirectoryLevels(const std::string &path)
{
    int currentLevel = 0;
    int lowestLevel = 0;
    std::string_view pathView(path);

    while (!pathView.empty()) {
        size_t slashIndex = pathView.find('/');
        if (slashIndex == std::string_view::npos) {
            break;
        }

        std::string_view segment = pathView.substr(0, slashIndex);
        if (segment.empty() || (segment.size() == 1 && segment[0] == '.')) {
            // Empty or dot segments don't change the level.
        }
        else if (segment.size() == 2 && segment[0] == '.' &&
                 segment[1] == '.') {
            currentLevel--;
            lowestLevel = std::min(lowestLevel, currentLevel);
        }
        else {
            currentLevel++;
        }

        pathView.remove_prefix(slashIndex + 1);
    }

    if (pathView == "..") {
        currentLevel--;
        lowestLevel = std::min(lowestLevel, currentLevel);
    }

    return -lowestLevel;
}

std::string FileUtils::lastNSegments(const std::string &path, const int n)
{
    if (n == 0) {
        return "";
    }

    std::string_view pathView(path);
    const auto pathLength = pathView.size();
    size_t substringStart = pathLength - 1;
    unsigned int substringLength = 1;
    int slashesSeen = 0;

    if (pathView[pathLength - 1] == '/') {
        substringLength = 0;
    }

    while (substringStart != 0) {
        if (pathView[substringStart - 1] == '/') {
            slashesSeen++;
            if (slashesSeen == n) {
                return std::string(
                    pathView.substr(substringStart, substringLength));
            }
        }
        substringStart--;
        substringLength++;
    }

    // The path might only be one segment (no slashes)
    if (slashesSeen == 0 && n == 1) {
        return std::string(pathView);
    }
    throw std::logic_error("Not enough segments in path");
}

bool FileUtils::isAbsolutePath(const std::string &path)
{
    return ((!path.empty()) && path[0] == '/');
}

std::string
FileUtils::resolvePathPrefixFromLocalToRemote(const std::string &path)
{
    if (RECC_PREFIX_REPLACEMENT.empty()) {
        return path;
    }

    // Iterate through dictionary, replacing path if it includes key, with
    // value.
    for (const auto &[localPrefix, remotePrefix] : RECC_PREFIX_REPLACEMENT) {
        // Check if prefix is found in the path, and that it is a prefix.
        if (FileUtils::hasPathPrefix(path, localPrefix)) {
            // Append a trailing slash to the replacement, in cases of
            // replacing `/` Double slashes will get removed during
            // normalization.
            const std::string replacedPath =
                remotePrefix + '/' + path.substr(localPrefix.length());
            std::string newPath =
                buildboxcommon::FileUtils::normalizePath(replacedPath.c_str());
            return newPath;
        }
    }
    return path;
}

std::string
FileUtils::resolvePathPrefixFromRemoteToLocal(const std::string &path)
{
    if (RECC_PREFIX_REPLACEMENT.empty()) {
        return path;
    }

    // Iterate through dictionary, replacing path if it includes key, with
    // value.
    for (const auto &[localPrefix, remotePrefix] : RECC_PREFIX_REPLACEMENT) {
        // Check if prefix is found in the path, and that it is a prefix.
        if (FileUtils::hasPathPrefix(path, remotePrefix)) {
            // Append a trailing slash to the replacement, in cases of
            // replacing `/` Double slashes will get removed during
            // normalization.
            const std::string replacedPath =
                localPrefix + '/' + path.substr(remotePrefix.length());
            std::string newPath =
                buildboxcommon::FileUtils::normalizePath(replacedPath.c_str());
            return newPath;
        }
    }
    return path;
}

std::vector<std::string> FileUtils::parseDirectories(std::string &path)
{
    std::vector<std::string> result;
    char *token = std::strtok(path.data(), "/");
    while (token != nullptr) {
        result.emplace_back(token);
        token = std::strtok(nullptr, "/");
    }

    return result;
}

std::string FileUtils::modifyPathForRemote(const std::string &path,
                                           const std::string &workingDirectory,
                                           bool normalizePath)
{
    std::string replacedPath =
        FileUtils::resolvePathPrefixFromLocalToRemote(path);
    replacedPath =
        FileUtils::rewritePathToRelative(replacedPath, workingDirectory);
    if (normalizePath && !RECC_NO_PATH_REWRITE) {
        replacedPath =
            buildboxcommon::FileUtils::normalizePath(replacedPath.c_str());
    }
    return replacedPath;
}

std::string FileUtils::makePathRelative(const std::string &path,
                                        const std::string &cwd)
{
    // Return unmodified `path` in the following cases
    if (cwd.empty() || path.empty() || path.front() != '/') {
        return path;
    }

    // If `cwd` is set, require it to be an absolute path
    if (cwd.front() != '/') {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "cwd must be an absolute path or empty: cwd=[" << cwd << "]");
    }

    // Both `path` and `cwd` are guaranteed to be absolute by the checks above
    std::string result = std::filesystem::relative(path, cwd);

    // Ensure that a trailing slash is retained
    if (path.back() == '/' && result.back() != '/') {
        result.push_back('/');
    }
    return result;
}

std::string
FileUtils::rewritePathToRelative(const std::string &path,
                                 const std::string &workingDirectory)
{
    std::string modifiedPath = path;
    if (!RECC_NO_PATH_REWRITE &&
        FileUtils::hasPathPrefix(modifiedPath, RECC_PROJECT_ROOT)) {
        modifiedPath =
            FileUtils::makePathRelative(modifiedPath, workingDirectory);
    }
    return modifiedPath;
}

const std::string FileUtils::resolvePathsInsideDependencyFileForRemote(
    const std::string &path, const std::string &modifiedUploadsDirectory)
{
    // Nothing to remap with.
    if (RECC_PREFIX_REPLACEMENT.empty() && RECC_NO_PATH_REWRITE) {
        return path;
    }

    // The local dependency files cannot be altered directly as this
    // would permanently modify the clients local copy of the files. So a
    // new modifiable file is created using a hash of the original path as
    // a unique name. This file is stored in a temporary directory that
    // must be kept alive until the upload is complete.
    std::string modifiedContentsPath =
        buildboxcommon::FileUtils::normalizePath(
            (std::string(modifiedUploadsDirectory) + "/" +
             buildboxcommon::DigestGenerator::hash(path).hash() + ".tmp")
                .c_str());

    BUILDBOX_LOG_DEBUG("Remapping local path prefixes of "
                       << path << " in new temporary file "
                       << modifiedContentsPath << " for remote upload.");

    // Modify the a new temporary file.
    try {
        buildboxcommon::FileUtils::copyFile(path.c_str(),
                                            modifiedContentsPath.c_str());

        // Get file contents as istringstream ready to be tokenized by
        // whitespace.
        std::istringstream fileContents{
            buildboxcommon::FileUtils::getFileContents(path.c_str())};
        std::string localPath;
        std::stringstream resolvedFileContents;

        // Stream through file contents and store the file paths from
        // within in localPath.
        bool onANewLine = true;
        while (fileContents >> localPath) {
            // Resolve the path if it has a prefix in the prefix map.
            const std::string remotePath =
                recc::FileUtils::modifyPathForRemote(localPath,
                                                     RECC_PROJECT_ROOT);

            // Log changed paths.
            if (localPath != remotePath) {
                BUILDBOX_LOG_DEBUG("Remapping local path \""
                                   << localPath << "\" to remote \""
                                   << remotePath << "\"\n");
            }

            // Reconstruct the dependency file with the resolved paths
            // separated by a space and when there is a \ a new line and don't
            // add a space on a new line.
            resolvedFileContents << (onANewLine ? "" : " ") << remotePath;
            if (localPath == "\\") {
                resolvedFileContents << "\n";
                onANewLine = true;
            }
            else {
                onANewLine = false;
            }
        }

        // Write the modifed file contents to a new file.
        buildboxcommon::FileUtils::writeFileAtomically(
            modifiedContentsPath, resolvedFileContents.str());
    }
    catch (const std::runtime_error &exception) {
        BUILDBOX_LOG_ERROR("Error remapping file: " << path << " error: "
                                                    << exception.what());
        return path;
    }

    // Return the path of the new temporary file with the modified contents to
    // be added to the action result.
    return modifiedContentsPath;
}

void FileUtils::resolvePathsInsideDependencyFileForLocal(
    const std::string &path)
{
    // Nothing to remap with.
    if (RECC_PREFIX_REPLACEMENT.empty()) {
        return;
    }

    BUILDBOX_LOG_DEBUG(
        "Remapping remote path prefixes inside downloaded dependency file \""
        << path << "\"");

    try {
        // Get file contents as istringstream ready to be tokenized by
        // whitespace.
        std::istringstream fileContents{
            buildboxcommon::FileUtils::getFileContents(path.c_str())};
        std::string remotePath;
        std::stringstream resolvedFileContents;

        // Stream through file contents and store the file paths from
        // within in remotePath.
        bool onANewLine = true;
        while (fileContents >> remotePath) {
            // Resolve the path if it has a prefix in the prefix map.
            const std::string localPath =
                recc::FileUtils::resolvePathPrefixFromRemoteToLocal(
                    remotePath);

            // Log changed paths.
            if (localPath != remotePath) {
                BUILDBOX_LOG_DEBUG("Remapping remote path \""
                                   << remotePath << "\" to local \""
                                   << localPath << "\"\n");
            }

            // Reconstruct the dependency file with the resolved paths
            // separated by a space and when there is a \ a new line and don't
            // add a space at the start of a new line.
            resolvedFileContents << (onANewLine ? "" : " ") << localPath;
            if (remotePath == "\\") {
                resolvedFileContents << "\n";
                onANewLine = true;
            }
            else {
                onANewLine = false;
            }
        }

        // Write the resolved file contents back to the dependency file.
        buildboxcommon::FileUtils::writeFileAtomically(
            path, resolvedFileContents.str());
    }
    catch (const std::runtime_error &exception) {
        BUILDBOX_LOG_ERROR("Error remapping file: " << path << " error: "
                                                    << exception.what());
    }
}

std::string FileUtils::resolveSymlink(const std::string &path)
{
    struct stat st = FileUtils::getStat(path.c_str(), false);
    auto target = FileUtils::getSymlinkContents(path, st);

    if (isAbsolutePath(target)) {
        return target;
    }
    else {
        const auto lastSlash = path.rfind('/');
        const auto dirname = (lastSlash == std::string::npos)
                                 ? ""
                                 : path.substr(0, lastSlash + 1);
        return dirname + target;
    }
}

std::string FileUtils::stripDirectory(const std::string &path)
{
    const auto slash = path.find_last_of('/');
    return slash == std::string::npos ? path : path.substr(slash + 1);
}

std::string FileUtils::replaceSuffix(const std::string &path,
                                     const std::string &suffix)
{
    const auto base = path.substr(0, path.rfind('.'));
    return base + suffix;
}

} // namespace recc
