// Copyright 2025 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 <buildboxcommon_jobserver.h>

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

#include <algorithm>
#include <stdlib.h>
#include <string>
#include <unistd.h>

namespace buildboxcommon {

std::optional<JobServerClient> JobServerClient::tryCreate()
{
    buildboxcommon::FileDescriptor readFD, writeFD;

    // https://www.gnu.org/software/make/manual/html_node/POSIX-Jobserver.html
    const char *makeflags = getenv("MAKEFLAGS");
    if (!makeflags) {
        return std::nullopt;
    }

    const std::string optionPrefix = "--jobserver-auth=";
    const std::string fifoPrefix = optionPrefix + "fifo:";
    const auto &options =
        buildboxcommon::StringUtils::split(std::string(makeflags), " ");

    // Find last `--jobserver-auth` option
    const auto &jobserverOption = std::find_if(
        options.crbegin(), options.crend(), [&](const std::string &makeflag) {
            return makeflag.starts_with(optionPrefix);
        });

    if (jobserverOption == options.crend() ||
        !jobserverOption->starts_with(fifoPrefix)) {
        BUILDBOX_LOG_DEBUG("No "
                           << fifoPrefix
                           << " option found in MAKEFLAGS: " << makeflags);
        return std::nullopt;
    }

    const auto fifoPath = jobserverOption->substr(fifoPrefix.size());
    readFD = buildboxcommon::FileDescriptor(
        open(fifoPath.c_str(), O_RDONLY | O_CLOEXEC));
    if (readFD.get() < 0) {
        BUILDBOX_LOG_WARNING(
            "Failed to open jobserver FIFO for reading: " << strerror(errno));
        return std::nullopt;
    }
    writeFD = buildboxcommon::FileDescriptor(
        open(fifoPath.c_str(), O_WRONLY | O_CLOEXEC));
    if (writeFD.get() < 0) {
        BUILDBOX_LOG_WARNING(
            "Failed to open jobserver FIFO for writing: " << strerror(errno));
        return std::nullopt;
    }

    return std::make_optional<JobServerClient>(std::move(readFD),
                                               std::move(writeFD));
}

JobServerClient::~JobServerClient()
{
    try {
        while (d_slotsAcquired > 0) {
            // Release explicitly acquired job slots
            if (!releaseSlot()) {
                // Job server failure, break to avoid infinite loop
                break;
            }
        }
        while (d_slotsAcquired < 0) {
            // Re-acquire the implicit job slot, if it was released
            if (!acquireSlot()) {
                // Job server failure, break to avoid infinite loop
                break;
            }
        }
    }
    catch (const std::exception &e) {
        BUILDBOX_LOG_ERROR(
            "Caught exception in JobServerClient destructor: " << e.what());
    }
}

bool JobServerClient::acquireSlot()
{
    BUILDBOX_LOG_DEBUG("Acquiring slot from job server");

    char token = '\0';
    ssize_t ret = 0;
    do {
        ret = read(this->d_readFD.get(), &token, 1);
    } while (ret < 0 && errno == EINTR);
    if (ret == 1) {
        d_slotsAcquired++;
        return true;
    }
    else {
        BUILDBOX_LOG_ERROR(
            "Failed to acquire job slot: " << (ret == 0 ? "read() returned 0"
                                                        : strerror(errno)));
        return false;
    }
}

bool JobServerClient::releaseSlot()
{
    if (d_slotsAcquired <= -1) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::logic_error,
                                       "JobServerClient: The implicit job "
                                       "slot has already been released");
    }

    BUILDBOX_LOG_DEBUG("Releasing slot to job server");

    char token = '+';
    ssize_t ret = 0;
    do {
        ret = write(this->d_writeFD.get(), &token, 1);
    } while (ret < 0 && errno == EINTR);
    if (ret == 1) {
        d_slotsAcquired--;
        return true;
    }
    else {
        BUILDBOX_LOG_ERROR(
            "Failed to release job slot: " << (ret == 0 ? "write() returned 0"
                                                        : strerror(errno)));
        return false;
    }
}
} // namespace buildboxcommon
