#ifndef INCLUDED_BUILDBOXCOMMON_SYSTEMUTILS
#define INCLUDED_BUILDBOXCOMMON_SYSTEMUTILS

#include <chrono>
#include <functional>
#include <optional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>

namespace buildboxcommon {

namespace system_utils_exitcode {
constexpr int COMMAND_NOT_FOUND = 127;
constexpr int COMMAND_CANNOT_EXECUTE = 126;
} // namespace system_utils_exitcode

struct ProcessCredentials {
    uid_t uid;
    gid_t gid;
};

typedef std::function<ProcessCredentials()> ProcessCredentialsGetter;

struct SystemUtils {

    /*
     * Represents the result of executing a command.
     */
    struct ExecutionResult {
        int d_exitCode = 0;
        std::string d_stdOut;    // Only valid if pipeStdOut was true
        std::string d_stdErr;    // Only valid if pipeStdErr was true
        bool d_timedOut = false; // True if process was killed due to timeout
    };

    /*
     * Executes the given command in the current process. `command[0]` must be
     * a path to a binary and the other entries its arguments.
     *
     * closeFds: close fds before executing the command
     * environment: environ passed to the command. If it is `std::nullopt_t`,
     *      the environ from the parent process is passed.
     *
     * If successful, it does not return.
     *
     * On error it returns exit codes that follow the convention used by Bash:
     * 126 if the command could not be executed (for example for lack of
     * permissions), or 127 if the command could not be found.
     */
    static int executeCommand(
        const std::vector<std::string> &command, bool closeFds = false,
        const std::optional<std::unordered_map<std::string, std::string>>
            &environment = std::nullopt);

    /*
     * Launches the given command in a separate process. `command[0]` must be
     * a path to a binary and the other entries its arguments.
     *
     * Returns the exit code returned by the command. Like `executeCommand()`,
     * follows the Bash convention for error codes.
     */
    static int executeCommandAndWait(
        const std::vector<std::string> &command, bool closeFds = false,
        const std::optional<std::unordered_map<std::string, std::string>>
            &environment = std::nullopt);

    /*
     * Launches the given command in a separate process, returning an
     * ExecutionResult
     *
     * `command[0]` must be a path to a binary and the other entries its
     *  arguments.
     *
     * If pipeStdOut is true, standard output will be redirected and returned
     * as part of the ExecutionResult. Similarly, pipeStdErr will redirect the
     * standard error stream.
     *
     * If the workingDirectory is specfied the possess will switch from the
     * current working directory to the specfied workingDirectory before
     * calling the comand.
     *
     * If stdinInput is provided, the data will be written to the command's
     * standard input. If std::nullopt, the child process inherits the parent's
     * stdin.
     *
     * If timeout is provided, the process will be terminated if it exceeds
     * the specified duration. Termination is attempted with SIGTERM first,
     * followed by SIGKILL if the process doesn't exit within a grace period.
     * When timeout occurs, d_timedOut will be set to true in the result.
     *
     * Returns the exit code returned by the command. Like `executeCommand()`,
     * follows the Bash convention for error codes.
     */
    static ExecutionResult executeCommandWithResult(
        const std::vector<std::string> &command, bool pipeStdOut = false,
        bool pipeStdErr = false,
        const std::optional<std::string> &workingDirectory = std::nullopt,
        const std::optional<std::string> &stdinInput = std::nullopt,
        const std::optional<std::chrono::milliseconds> &timeout =
            std::nullopt);

    /* Looks for the absolute path to a given command using the `$PATH`
     * environment variable.
     *
     * If `command` is already a path, it returns it unmodified.
     *
     * If the corresponding executable is not found, returns an empty string.
     */
    static std::string getPathToCommand(const std::string &command);

    /*
     * Waits for the given PID and returns an exit code following the
     * convention used by Bash:
     *  - If it exits: its status code,
     *  - If it is signaled: 128 + the signal number
     *
     * On errors, throws an `std::system_error` exception.
     */
    static int waitPid(const pid_t pid);

    /*
     * Equivalent to `waitPid()` except that it returns `-EINTR` if a signal
     * was caught.
     */
    static int waitPidOrSignal(const pid_t pid);

    /*
     * Waits up to timeout for the given PID to exit or be signaled.
     *
     * In the case of a timeout `-ETIMEDOUT` is returned and `timedOut`
     * is set to true.
     */
    static int waitPidOrSignalWithTimeout(
        const pid_t pid,
        const std::optional<std::chrono::milliseconds> &timeout,
        bool *timedOut);

    /*
     * Get current working directory. Uses getcwd()
     *
     * On error, throws as 'std::runtime_error` exception
     */
    static std::string getCurrentWorkingDirectory();

    /*
     * Redirects stdout or stderr to a given path. The file
     * will be created if necessary and truncated if already exists (equivalent
     * to a > redirection).
     *
     * If the specified file descriptor is not STDOUT_FILENO or STDERR_FILENO,
     * throws `std::invalid_argument`.
     *
     * On errors throws `std::system_error`.
     */
    static void redirectStandardOutputToFile(const int standardOutputFd,
                                             const std::string &path);

    /*
     * Get effective `uid` and `gid` as process credentials
     */
    static ProcessCredentials getProcessCredentials();

    /*
     * Get the effective and supplementary group IDs.
     *
     * On error, throws as 'std::runtime_error` exception.
     */
    static std::unordered_set<gid_t> getGids();

    /*
     * Get uid from username.
     *
     * On error, throws as 'std::runtime_error` exception.
     */

    static uid_t getUidFromName(const std::string &name);
    /*
     * Get gid from group name.
     *
     * On error, throws as 'std::runtime_error` exception.
     */
    static uid_t getGidFromName(const std::string &name);

    /*
     * Create a pipe and set `O_CLOEXEC` on the file descriptors.
     *
     * On error, throws `std::system_error`.
     */
    static std::array<int, 2> createPipe();
};

class PlatformInfo {
    /*
     * Singleton class representing runtime information of the current
     * platform.
     */

  public:
    static const PlatformInfo &get();
    bool enableCopyFileRange() const;
    bool enableOpenat2() const;
    int majorVersion() const;
    int minorVersion() const;

    ~PlatformInfo() = default;
    PlatformInfo &operator=(const PlatformInfo &) = delete;
    PlatformInfo(const PlatformInfo &) = delete;
    PlatformInfo(PlatformInfo &&) = delete;
    PlatformInfo &operator=(PlatformInfo &&) = delete;

  private:
    PlatformInfo();
    std::string d_osName;
    int d_majorVer = 0;
    int d_minorVer = 0;

    bool d_enableCopyFileRange = false;
    bool d_enableOpenat2 = false;
};

} // namespace buildboxcommon

#endif // INCLUDED_BUILDBOXCOMMON_SYSTEMUTILS
