#pragma once
///@file

#include "lix/libutil/async-io.hh"
#include "lix/libutil/result.hh"
#include "lix/libutil/types.hh"
#include "lix/libutil/error.hh"
#include "lix/libutil/file-descriptor.hh"

#include <kj/async.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>

#include <functional>
#include <map>
#include <optional>

namespace nix {

struct Sink;
struct Source;

class Pid
{
    pid_t pid = -1;
public:
    Pid();
    explicit Pid(pid_t pid): pid(pid) {}
    Pid(Pid && other);
    Pid & operator=(Pid && other);
    ~Pid() noexcept(false);
    explicit operator bool() const { return pid != -1; }
    int kill();
    int wait();

    pid_t release();
    pid_t get() const { return pid; }
};

class ProcessGroup
{
    Pid leader;

public:
    ProcessGroup() = default;
    explicit ProcessGroup(Pid leader) : leader(std::move(leader)) {}
    ProcessGroup(ProcessGroup &&) = default;
    ProcessGroup & operator=(ProcessGroup &&) = default;
    ~ProcessGroup() noexcept(false);

    explicit operator bool() const
    {
        return bool(leader);
    }

    int wait()
    {
        return leader.wait();
    }
    int kill();
};

/**
 * Kill all processes running under the specified uid by sending them
 * a SIGKILL.
 */
void killUser(uid_t uid);

/**
 * Run a program and return its stdout in a string (i.e., like the
 * shell backtick operator).
 */
kj::Promise<Result<std::string>> runProgram(
    Path program,
    bool searchPath = false,
    const Strings args = Strings(),
    bool isInteractive = false
);

struct RunOptions
{
    struct Redirection
    {
        int dup, from;
    };

    Path program;
    bool searchPath = true;
    std::optional<std::string> argv0;
    Strings args = {};
    std::optional<std::map<std::string, std::string>> environment = {};
    bool captureStdout = false;
    std::vector<Redirection> redirections;
    bool keepContext = false;
};

struct [[nodiscard("you must call RunningProgram::wait()")]] RunningProgram
{
    friend RunningProgram runProgram2(const RunOptions & options);

protected:
    Path program;
    Pid pid;
    std::unique_ptr<AsyncFdIoStream> childStdout;

    RunningProgram(PathView program, Pid pid, AutoCloseFD childStdout);

public:
    RunningProgram() = default;
    RunningProgram(RunningProgram &&) = default;
    RunningProgram & operator=(RunningProgram &&) = default;
    ~RunningProgram();

    explicit operator bool() const { return bool(pid); }

    std::tuple<Pid, std::unique_ptr<AsyncFdIoStream>> release();

    int kill();
    [[nodiscard]]
    int wait();
    void waitAndCheck();

    std::optional<int> getStdoutFD() const
    {
        return childStdout ? std::optional(childStdout->getFD()) : std::nullopt;
    }

    AsyncFdIoStream * getStdout() const
    {
        return childStdout.get();
    };
};

kj::Promise<Result<std::pair<int, std::string>>> runProgram(RunOptions options);

RunningProgram runProgram2(const RunOptions & options);

class [[nodiscard("you must call RunningHelper::wait()")]] RunningHelper : RunningProgram
{
    friend RunningHelper runHelper(const char * name, RunOptions options);

    const char * name;
    AutoCloseFD errPipe;

    RunningHelper(const char * name, RunningProgram && proc, AutoCloseFD && errPipe)
        : RunningProgram(std::move(proc))
        , name(name)
        , errPipe(std::move(errPipe))
    {
    }

    void check();

public:
    RunningHelper() = default;
    RunningHelper(RunningHelper &&) = default;
    RunningHelper & operator=(RunningHelper &&) = default;

    using RunningProgram::operator bool;
    using RunningProgram::getStdout;
    using RunningProgram::getStdoutFD;
    using RunningProgram::kill;
    using RunningProgram::wait;

    /**
     * Kill the entire process group the helper runs in. This is usually **unsafe**
     * unless the helper process has made itself a process group or session leader!
     */
    int killProcessGroup();

    void waitAndCheck();
};

RunningHelper runHelper(const char * name, RunOptions options);

class ExecError : public Error
{
public:
    int status;

    template<typename... Args>
    ExecError(int status, const Args & ... args)
        : Error(args...), status(status)
    { }
};

/**
 * Convert the exit status of a child as returned by wait() into an
 * error string.
 */
std::string statusToString(int status);

bool statusOk(int status);

}
