Plugin to add Pijul support to the Nix package manager
#include "repo.h"

#include <nlohmann/json.hpp>
#include <utility>

using nlohmann::json;

using namespace std::string_literals;
using namespace std::string_view_literals;

namespace nixpluginpijul
{

void runPijul(const StringList &args, const std::optional<Path> &chdir, OutputMode mode = OutputMode::Standard)
{
    runProcess("pijul", args, chdir ? chdir->c_str() : nullptr, mode);
}

void record(const std::string_view &message, const Path &chdir, const std::vector<Path> &paths)
{
    StringList args{"record", "--message"};

    args.emplace_back(message);
    args.emplace_back("--");

    for (auto &s : paths) {
        args.emplace_back(s);
    }

    runPijul(args, chdir, OutputMode::Interactive);
}

void clone(const std::string_view &repoUrl,
           const Path &repoDir,
           const std::optional<std::string_view> &channel,
           const std::optional<std::string_view> &state)
{
    StringList args{"clone"s};

    if (channel) {
        args.push_back("--channel"s);
        args.emplace_back(*channel);
    }

    if (state) {
        args.push_back("--state"s);
        args.emplace_back(*state);
    }

    args.emplace_back(repoUrl);
    args.emplace_back(repoDir);

    runPijul(args, {}, OutputMode::Interactive);
}

bool isRepoDirty(const Path &chdir = {})
{
    std::string diffOutput;
    runPijul({"diff", "--json"}, chdir, OutputMode::WithOutput{diffOutput});

    bool dirty = false;

    if (!diffOutput.empty()) {
        const auto &json = nlohmann::json::parse(diffOutput);

        if (!json.empty()) {
            dirty = true;
        }
    }

    return dirty;
}

std::set<Path> getTrackedFiles(const Path &repoDir)
{
    std::string output;
    runPijul({"list"}, repoDir, OutputMode::WithOutput{output});

    size_t pos = 0;
    size_t next;

    std::set<Path> out;

    while ((next = output.find_first_of("\n\r", pos)) != std::string::npos) {
        out.emplace(output.substr(pos, next - pos));
        pos = next + 1;
    }

    return out;
}

RepoStatus getRepoStatus(const Path &repoPath)
{
    auto [state, lastModified] = getState(repoPath);
    auto channel = getRepoChannel(repoPath);

    return RepoStatus{
        .channel = std::move(channel),
        .state = std::move(state),
        .lastModified = lastModified,
    };
}

std::pair<std::string, uint64_t> getState(const Path &repoPath)
{
    std::string output;
    runPijul({"log", "--output-format", "json", "--state", "--limit", "1"}, Path(repoPath), OutputMode::WithOutput{output});

    const auto &json = json::parse(output);
    const auto &commitInfo = json.at(0);

    const auto &timestampSpec = commitInfo.at("timestamp").get<std::string>();
    const uint64_t timestamp = std::chrono::duration_cast<std::chrono::seconds>(parseRFC3339(timestampSpec).time_since_epoch()).count();

    const std::string &state = commitInfo.at("state");

    return {state, timestamp};
}

std::string getRepoChannel(const Path &repoPath)
{
    std::string output;
    runPijul({"channel"}, Path(repoPath), OutputMode::WithOutput{output});

    std::string::size_type pos = 0;

    do {
        const auto nl = output.find('\n', pos);
        const auto line = std::string_view(output).substr(pos, nl - pos);

        if (line.empty()) {
            continue;
        }

        if (line.at(0) == '*') {
            return std::string(line.substr(2));
        }

        pos = nl;
    } while (pos != std::string::npos);

    fatal("could not parse current channel");
}

date::sys_time<std::chrono::milliseconds> parseRFC3339(const std::string &spec)
{
    std::istringstream in{spec};
    date::sys_time<std::chrono::milliseconds> pt;

    in >> date::parse("%FT%TZ", pt);

    if (in.fail()) {
        in.clear();
        in.exceptions(std::ios::failbit);
        in.str(spec);
        in >> date::parse("%FT%T%Ez", pt);
    }

    return pt;
}

}