#include "lix/libutil/c-calls.hh"
#include "lix/libutil/error.hh"
#include "lix/libutil/file-descriptor.hh"
#include "lix/libutil/file-system.hh"
#include "lix/libstore/globals.hh"
#include "lix/libstore/build/hook-instance.hh"
#include "lix/libutil/json.hh"
#include "lix/libutil/logging.hh"
#include "lix/libutil/processes.hh"
#include "lix/libutil/result.hh"
#include "lix/libutil/rpc.hh"
#include "lix/libutil/serialise.hh"
#include "lix/libutil/strings.hh"
#include "lix/libutil/logging-rpc.hh" // IWYU pragma: keep
#include "lix/libutil/types-rpc.hh" // IWYU pragma: keep
#include <fcntl.h>
#include <kj/common.h>
#include <kj/memory.h>
#include <memory>
#include <string_view>
#include <unistd.h>

namespace nix {

void HookInstance::HookLogger::emitLog(rpc::log::Event::Result::Reader r)
{
    auto type = rpc::log::from(r.getType());
    auto fields = r.getFields();
    if (!type) {
        return;
    }

    // ensure that logs from a builder using `ssh-ng://` as protocol
    // are also available to `nix log`.
    if (type == resBuildLogLine) {
        if (fields.size() > 0 && fields[0].isS()) {
            (*logSink)(fmt("%s\n", rpc::to<std::string_view>(fields[0].getS())));
        } else {
            (*logSink)("\n");
        }
    } else if (type == resSetPhase && fields.size() > 0 && fields[0].isS()) {
        // nixpkgs' stdenv produces lines in the log to signal phase changes.
        // We want to get the same lines in case of remote builds.
        // The format is:
        //   @nix { "action": "setPhase", "phase": "$curPhase" }
        const auto phase = rpc::to<std::string_view>(fields[0].getS());
        const auto logLine = JSON::object({{"action", "setPhase"}, {"phase", phase}});
        (*logSink)("@nix " + logLine.dump(-1, ' ', false, JSON::error_handler_t::replace) + "\n");
    }
}

kj::Promise<void> HookInstance::HookLogger::push(PushContext context)
{
    try {
        auto e = context.getParams().getE();
        if (logSink && e.isResult()) {
            emitLog(e.getResult());
        }
    } catch (std::exception & e) { // NOLINT(lix-foreign-exceptions)
        printError("error in log processor: %s", e.what());
        throw; // NOLINT(lix-foreign-exceptions)
    }

    return RpcLoggerServer::push(context);
}

kj::Promise<Result<std::unique_ptr<HookInstance>>> HookInstance::create(const Activity & act)
try {
    debug("starting build hook '%s'", concatStringsSep(" ", settings.buildHook.get()));

    auto buildHookArgs = settings.buildHook.get();

    if (buildHookArgs.empty())
        throw Error("'build-hook' setting is empty");

    auto buildHook = canonPath(buildHookArgs.front());
    buildHookArgs.emplace(std::next(buildHookArgs.begin()), baseNameOf(buildHook));
    buildHookArgs.push_back(std::to_string(verbosity));

    /* Create the communication pipes. */
    auto [selfRPC, hookRPC] = SocketPair::stream();

    /* Fork the hook. */
    auto pid = runHelper(
        "run-build-hook",
        {
            .args = buildHookArgs,
            .redirections = {{.dup = STDOUT_FILENO, .from = hookRPC.get()}},
        }
    );
    KJ_DEFER({
        // kill the hook if the promise is cancelled. the hook helper creates
        // a session, so we'll kill the entire process group just to be safe.
        if (pid) {
            pid.killProcessGroup();
        }
    });

    std::map<std::string, Config::SettingInfo> settings;
    globalConfig.getSettings(settings, true);

    auto conn = AIO().lowLevelProvider.wrapUnixSocketFd(kj::AutoCloseFd(selfRPC.release()));
    auto client = std::make_unique<capnp::TwoPartyClient>(*conn, 1);
    auto rpc = client->bootstrap().castAs<rpc::build_remote::HookInstance>();

    {
        auto initReq = rpc.initRequest();
        initReq.setLogger(kj::heap<HookLogger>(act, nullptr));
        RPC_FILL(initReq, initSettings, settings);
        TRY_AWAIT_RPC(initReq.send());
    }

    co_return std::make_unique<HookInstance>(
        kj::heap(std::move(rpc)).attach(std::move(conn), std::move(client)), std::move(pid)
    );
} catch (...) {
    co_return result::current_exception();
}

HookInstance::~HookInstance()
{
    try {
        kill();
    } catch (...) {
        ignoreExceptionInDestructor();
    }
}

}
