#include "PluginManager.hpp"
#include "../helpers/Colors.hpp"
#include "../helpers/StringUtils.hpp"
#include "../progress/CProgressBar.hpp"
#include "Manifest.hpp"
#include "DataState.hpp"
#include "HyprlandSocket.hpp"
#include "../helpers/Sys.hpp"
#include "../helpers/Die.hpp"

#include <cstdio>
#include <iostream>
#include <filesystem>
#include <string>
#include <print>
#include <fstream>
#include <algorithm>
#include <format>
#include <string_view>

#include <sys/types.h>
#include <sys/stat.h>
#include <pwd.h>
#include <unistd.h>

#include <toml++/toml.hpp>
#include <glaze/glaze.hpp>

#include <hyprutils/string/String.hpp>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/Casts.hpp>
using namespace Hyprutils::String;
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;

static std::string execAndGet(std::string cmd) {
    cmd += " 2>&1";

    CProcess proc("/bin/sh", {"-c", cmd});

    if (!proc.runSync())
        return "error";

    return proc.stdOut();
}

static std::string getTempRoot() {
    static auto ENV = getenv("XDG_RUNTIME_DIR");
    if (!ENV) {
        std::cerr << "\nERROR: XDG_RUNTIME_DIR not set!\n";
        exit(1);
    }

    const auto STR = std::format("{}/hyprpm/", ENV);

    return STR;
}

static bool isValidHash(std::string_view sv) {
    return std::ranges::all_of(sv, [](const char& c) { return std::isdigit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); });
}

CPluginManager::CPluginManager() {
    if (NSys::isSuperuser())
        Debug::die("Don't run hyprpm as a superuser.");

    m_szUsername = getpwuid(NSys::getUID())->pw_name;
}

SHyprlandVersion CPluginManager::getHyprlandVersion(bool running) {
    static bool             onceRunning   = false;
    static bool             onceInstalled = false;
    static SHyprlandVersion verRunning;
    static SHyprlandVersion verInstalled;

    if (onceRunning && running)
        return verRunning;

    if (onceInstalled && !running)
        return verInstalled;

    if (running)
        onceRunning = true;
    else
        onceInstalled = true;

    const auto HLVERCALL = running ? NHyprlandSocket::send("j/version") : execAndGet("Hyprland --version-json");

    auto       jsonQuery = glz::read_json<glz::generic>(HLVERCALL);

    if (!jsonQuery) {
        std::println("{}", failureString("failed to get the current hyprland version. Are you running hyprland?"));
        return SHyprlandVersion{};
    }

    auto   hlbranch  = (*jsonQuery)["branch"].get_string();
    auto   hlcommit  = (*jsonQuery)["commit"].get_string();
    auto   abiHash   = (*jsonQuery)["abiHash"].get_string();
    auto   hldate    = (*jsonQuery)["commit_date"].get_string();
    auto   hlcommits = (*jsonQuery)["commits"].get_string();

    auto   flags = (*jsonQuery)["flags"].get_array();
    bool   isNix = std::ranges::any_of(flags, [](const auto& f) { return f.is_string() && f.get_string() == std::string_view{"nix"}; });

    size_t commits = 0;
    try {
        commits = std::stoull(hlcommits);
    } catch (...) { ; }

    if (m_bVerbose)
        std::println("{}", verboseString("parsed commit {} at branch {} on {}, commits {}, nix: {}", hlcommit, hlbranch, hldate, commits, isNix));

    auto ver = SHyprlandVersion{hlbranch, hlcommit, hldate, abiHash, commits, isNix};

    if (running)
        verRunning = ver;
    else
        verInstalled = ver;

    return ver;
}

bool CPluginManager::createSafeDirectory(const std::string& path) {
    if (path.empty() || !path.starts_with(getTempRoot()))
        return false;

    if (std::filesystem::exists(path))
        std::filesystem::remove_all(path);

    if (std::filesystem::exists(path))
        return false;

    if (mkdir(path.c_str(), S_IRWXU) < 0)
        return false;

    return true;
}

bool CPluginManager::validArg(const std::string& s) {
    return !s.contains("'") && !s.ends_with("\\") && !s.starts_with("\\");
}

bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string& rev) {
    const auto HLVER = getHyprlandVersion();

    if (!validArg(url) || !validArg(rev)) {
        std::println(stderr, "\n{}", failureString("url or rev invalid"));
        return false;
    }

    if (!hasDeps()) {
        std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. Dependencies not satisfied. Hyprpm requires: cmake, cpio, pkg-config, git, g++, gcc"));
        return false;
    }

    if (DataState::pluginRepoExists(SPluginRepoIdentifier::fromUrl(url))) {
        std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. Repository already installed."));
        return false;
    }

    auto GLOBALSTATE = DataState::getGlobalState();
    if (!GLOBALSTATE.dontWarnInstall) {
        std::println("{}!{} Disclaimer: {}", Colors::YELLOW, Colors::RED, Colors::RESET);
        std::println("plugins, especially not official, have no guarantee of stability, availability or security.\n"
                     "Run them at your own risk.\n"
                     "This message will not appear again.");
        GLOBALSTATE.dontWarnInstall = true;
        DataState::updateGlobalState(GLOBALSTATE);
    }

    if (GLOBALSTATE.headersAbiCompiled.empty()) {
        std::println("\n{}", failureString("Cannot find headers in the global state. Try running hyprpm update first."));
        return false;
    }

    std::cout << Colors::GREEN << "✔" << Colors::RESET << Colors::RED << " adding a new plugin repository " << Colors::RESET << "from " << url << "\n  " << Colors::RED
              << "MAKE SURE" << Colors::RESET << " that you trust the authors. " << Colors::RED << "DO NOT" << Colors::RESET
              << " install random plugins without verifying the code and author.\n  "
              << "Are you sure? [Y/n] ";
    std::fflush(stdout);
    std::string input;
    std::getline(std::cin, input);

    if (input.size() > 0 && input[0] != 'Y' && input[0] != 'y') {
        std::println(stderr, "Aborting.");
        return false;
    }

    CProgressBar progress;
    progress.m_iMaxSteps        = 5;
    progress.m_iSteps           = 0;
    progress.m_szCurrentMessage = "Cloning the plugin repository";

    progress.print();

    if (!std::filesystem::exists(getTempRoot())) {
        std::filesystem::create_directory(getTempRoot());
        std::filesystem::permissions(getTempRoot(), std::filesystem::perms::owner_all, std::filesystem::perm_options::replace);
    } else if (!std::filesystem::is_directory(getTempRoot())) {
        std::println(stderr, "\n{}", failureString("Could not prepare working dir for hyprpm"));
        return false;
    }

    const std::string USERNAME = getpwuid(getuid())->pw_name;

    m_szWorkingPluginDirectory = std::format("{}{}", getTempRoot(), USERNAME);

    if (!createSafeDirectory(m_szWorkingPluginDirectory)) {
        std::println(stderr, "\n{}", failureString("Could not prepare working dir for repo"));
        return false;
    }

    progress.printMessageAbove(infoString("Cloning {}", url));

    std::string ret = execAndGet(std::format("cd {} && git clone --recursive '{}' {}", getTempRoot(), url, USERNAME));

    if (!std::filesystem::exists(std::format("{}/.git", m_szWorkingPluginDirectory))) {
        std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. shell returned:\n{}", ret));
        return false;
    }

    if (!rev.empty()) {
        std::string ret = execAndGet(std::format("git -C {} reset --hard --recurse-submodules {}", m_szWorkingPluginDirectory, rev));
        if (ret.compare(0, 6, "fatal:") == 0) {
            std::println(stderr, "\n{}", failureString("Could not check out revision {}. shell returned:\n{}", rev, ret));
            return false;
        }
        ret = execAndGet(std::format("git -C {} submodule update --init", m_szWorkingPluginDirectory));
        if (m_bVerbose)
            std::println("{}", verboseString("git submodule update --init returned: {}", ret));
    }

    progress.m_iSteps = 1;
    progress.printMessageAbove(successString("cloned"));
    progress.m_szCurrentMessage = "Reading the manifest";
    progress.print();

    std::unique_ptr<CManifest> pManifest;

    if (std::filesystem::exists(std::format("{}/hyprpm.toml", m_szWorkingPluginDirectory))) {
        progress.printMessageAbove(successString("found hyprpm manifest"));
        pManifest = std::make_unique<CManifest>(MANIFEST_HYPRPM, std::format("{}/hyprpm.toml", m_szWorkingPluginDirectory));
    } else if (std::filesystem::exists(std::format("{}/hyprload.toml", m_szWorkingPluginDirectory))) {
        progress.printMessageAbove(successString("found hyprload manifest"));
        pManifest = std::make_unique<CManifest>(MANIFEST_HYPRLOAD, std::format("{}/hyprload.toml", m_szWorkingPluginDirectory));
    }

    if (!pManifest) {
        std::println(stderr, "\n{}", failureString("The provided plugin repository does not have a valid manifest"));
        return false;
    }

    if (!pManifest->m_good) {
        std::println(stderr, "\n{}", failureString("The provided plugin repository has a corrupted manifest"));
        return false;
    }

    progress.m_iSteps = 2;
    progress.printMessageAbove(successString("parsed manifest, found {} plugins:", pManifest->m_plugins.size()));
    for (auto const& pl : pManifest->m_plugins) {
        std::string message = std::format("→ {} by ", pl.name);
        for (auto const& a : pl.authors) {
            message += std::format("{}, ", a);
        }
        if (pl.authors.size() > 0) {
            message.pop_back();
            message.pop_back();
        }
        message += std::format(" version {}", pl.version);
        progress.printMessageAbove(message);
    }

    if (rev.empty() && !pManifest->m_repository.commitPins.empty()) {
        // check commit pins

        progress.printMessageAbove(infoString("Manifest has {} pins, checking", pManifest->m_repository.commitPins.size()));

        for (auto const& [hl, plugin] : pManifest->m_repository.commitPins) {
            if (hl != HLVER.hash)
                continue;

            if (plugin.contains("'") || !isValidHash(plugin)) {
                std::println(stderr, "\n{}", failureString("Plugin has a malformed manifest: bad commit pin"));
                return false;
            }

            progress.printMessageAbove(successString("commit pin {} matched hl, resetting", plugin));

            execAndGet(std::format("cd {} && git reset --hard --recurse-submodules '{}'", m_szWorkingPluginDirectory, plugin));

            ret = execAndGet(std::format("git -C {} submodule update --init", m_szWorkingPluginDirectory));
            if (m_bVerbose)
                std::println("{}", verboseString("git submodule update --init returned: {}", ret));

            break;
        }
    }

    progress.m_szCurrentMessage = "Verifying headers";
    progress.print();

    const auto HEADERSSTATUS = headersValid();

    if (HEADERSSTATUS != HEADERS_OK) {
        std::println("\n{}", headerError(HEADERSSTATUS));
        std::println("\n{}", infoString("if the problem persists, try running hyprpm purge-cache."));
        return false;
    }

    progress.m_iSteps = 3;
    progress.printMessageAbove(successString("Hyprland headers OK"));
    progress.m_szCurrentMessage = "Building plugin(s)";
    progress.print();

    for (auto& p : pManifest->m_plugins) {
        std::string out;

        if (p.since > HLVER.commits && HLVER.commits >= 1 /* for --depth 1 clones, we can't check this. */) {
            progress.printMessageAbove(failureString("Not building {}: your Hyprland version is too old.\n", p.name));
            p.failed = true;
            continue;
        }

        progress.printMessageAbove(infoString("Building {}", p.name));

        const auto env = getPluginBuildEnv();

        for (auto const& bs : p.buildSteps) {
            const auto CMD_RAW = nixDevelopIfNeeded(std::format("cd {} && {} {}", m_szWorkingPluginDirectory, env, bs), HLVER);

            if (!CMD_RAW) {
                progress.printMessageAbove(failureString("Failed to build {}: {}", p.name, CMD_RAW.error()));
                break;
            }

            out += std::format(" -> {}\n{}\n", *CMD_RAW, execAndGet(*CMD_RAW));
        }

        if (m_bVerbose)
            std::println("{}", verboseString("shell returned: {}", out));

        if (!std::filesystem::exists(std::format("{}/{}", m_szWorkingPluginDirectory, p.output))) {
            progress.printMessageAbove(failureString("Plugin {} failed to build.\n"
                                                     "  This likely means that the plugin is either outdated, not yet available for your version, or broken.\n"
                                                     "  If you are on -git, update first\n"
                                                     "  Try re-running with -v to see more verbose output.\n",
                                                     p.name));

            p.failed = true;
            continue;
        }

        progress.printMessageAbove(successString("built {} into {}", p.name, p.output));
    }

    progress.printMessageAbove(successString("all plugins built"));
    progress.m_iSteps           = 4;
    progress.m_szCurrentMessage = "Installing repository";
    progress.print();

    // add repo toml to DataState
    SPluginRepository repo;
    std::string       repohash = execAndGet(std::format("cd {} && git rev-parse HEAD", m_szWorkingPluginDirectory));
    if (repohash.length() > 0)
        repohash.pop_back();
    auto lastSlash       = url.find_last_of('/');
    auto secondLastSlash = url.find_last_of('/', lastSlash - 1);
    repo.name            = pManifest->m_repository.name.empty() ? url.substr(lastSlash + 1) : pManifest->m_repository.name;
    repo.author          = url.substr(secondLastSlash + 1, lastSlash - secondLastSlash - 1);
    repo.url             = url;
    repo.rev             = rev;
    repo.hash            = repohash;
    for (auto const& p : pManifest->m_plugins) {
        repo.plugins.push_back(SPlugin{p.name, std::format("{}/{}", m_szWorkingPluginDirectory, p.output), false, p.failed});
    }
    DataState::addNewPluginRepo(repo);

    progress.printMessageAbove(successString("installed repository"));
    progress.printMessageAbove(successString("you can now enable the plugin(s) with hyprpm enable"));
    progress.m_iSteps           = 5;
    progress.m_szCurrentMessage = "Done!";
    progress.print();

    std::print("\n");

    // remove build files
    std::filesystem::remove_all(m_szWorkingPluginDirectory);

    return true;
}

bool CPluginManager::removePluginRepo(const SPluginRepoIdentifier& identifier) {
    if (!DataState::pluginRepoExists(identifier)) {
        std::println(stderr, "\n{}", failureString("Could not remove the repository. Repository is not installed."));
        return false;
    }

    std::cout << Colors::YELLOW << "!" << Colors::RESET << Colors::RED << " removing a plugin repository: " << Colors::RESET << identifier.toString() << "\n  "
              << "Are you sure? [Y/n] ";
    std::fflush(stdout);
    std::string input;
    std::getline(std::cin, input);

    if (input.size() > 0 && input[0] != 'Y' && input[0] != 'y') {
        std::println("Aborting.");
        return false;
    }

    DataState::removePluginRepo(identifier);

    return true;
}

eHeadersErrors CPluginManager::headersValid() {
    const auto HLVER = getHyprlandVersion(false);

    if (!std::filesystem::exists(std::format("{}/share/pkgconfig/hyprland.pc", DataState::getHeadersPath())))
        return HEADERS_MISSING;

    // find headers commit
    const std::string& cmd     = std::format("PKG_CONFIG_PATH=\"{}\" pkgconf --cflags --keep-system-cflags hyprland", getPkgConfigPath());
    auto               headers = execAndGet(cmd);

    if (!headers.contains("-I/"))
        return HEADERS_MISSING;

    headers.pop_back(); // pop newline

    std::string verHeader;

    while (!headers.empty()) {
        const auto PATH = headers.substr(0, headers.find(" -I/", 3));

        if (headers.find(" -I/", 3) != std::string::npos)
            headers = headers.substr(headers.find("-I/", 3));
        else
            headers = "";

        if (PATH.ends_with("protocols"))
            continue;

        verHeader = std::format("{}/hyprland/src/version.h", trim(PATH.substr(2)));
        break;
    }

    if (verHeader.empty())
        return HEADERS_CORRUPTED;

    // read header
    std::ifstream ifs(verHeader);
    if (!ifs.good())
        return HEADERS_CORRUPTED;

    std::string verHeaderContent((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
    ifs.close();

    const auto HASHPOS = verHeaderContent.find("#define GIT_COMMIT_HASH");

    if (HASHPOS == std::string::npos || HASHPOS + 23 >= verHeaderContent.length())
        return HEADERS_CORRUPTED;

    std::string_view hash = verHeaderContent;
    hash.remove_prefix(HASHPOS + 23);
    hash = hash.substr(0, hash.find_first_of('\n'));

    const auto FIRSTQUOTE = hash.find_first_of('"');
    if (FIRSTQUOTE == std::string_view::npos)
        return HEADERS_CORRUPTED;

    hash.remove_prefix(FIRSTQUOTE + 1);

    const auto SECONDQUOTE = hash.find_first_of('"');
    if (SECONDQUOTE == std::string_view::npos)
        return HEADERS_CORRUPTED;

    hash = hash.substr(0, SECONDQUOTE);

    if (hash != HLVER.hash)
        return HEADERS_MISMATCHED;

    // check ABI hash too
    const auto GLOBALSTATE = DataState::getGlobalState();

    if (GLOBALSTATE.headersAbiCompiled != HLVER.abiHash)
        return HEADERS_ABI_MISMATCH;

    return HEADERS_OK;
}

bool CPluginManager::updateHeaders(bool force) {
    DataState::ensureStateStoreExists();

    const auto HLVER = getHyprlandVersion(false);

    if (!hasDeps()) {
        std::println("\n{}", failureString("Could not update. Dependencies not satisfied. Hyprpm requires: cmake, cpio, pkg-config, git, g++, gcc"));
        return false;
    }

    if (!std::filesystem::exists(getTempRoot())) {
        std::filesystem::create_directory(getTempRoot());
        std::filesystem::permissions(getTempRoot(), std::filesystem::perms::owner_all, std::filesystem::perm_options::replace);
    }

    const auto CURRENTHEADERS = headersValid();

    if (!force && (CURRENTHEADERS == HEADERS_OK || CURRENTHEADERS == HEADERS_ABI_MISMATCH)) {
        std::println("\n{}", successString("Headers up to date."));
        return true;
    }

    CProgressBar progress;
    progress.m_iMaxSteps        = 5;
    progress.m_iSteps           = 0;
    progress.m_szCurrentMessage = "Cloning the hyprland repository";
    progress.print();

    const std::string USERNAME   = getpwuid(getuid())->pw_name;
    const auto        WORKINGDIR = std::format("{}hyprland-{}", getTempRoot(), USERNAME);

    if (!createSafeDirectory(WORKINGDIR)) {
        std::println("\n{}", failureString("Could not prepare working dir for hl"));
        return false;
    }

    const auto& HL_URL = m_szCustomHlUrl.empty() ? "https://github.com/hyprwm/Hyprland" : m_szCustomHlUrl;

    progress.printMessageAbove(statusString("!", Colors::YELLOW, "Cloning {}, this might take a moment.", HL_URL));

    const bool bShallow = (HLVER.branch == "main") && !m_bNoShallow;

    // let us give a bit of leg-room for shallowing
    // due to timezones, etc.
    const std::string SHALLOW_DATE =
        trim(HLVER.date).empty() ? "" : execAndGet(std::format("LC_TIME=\"en_US.UTF-8\" date --date='{} - 1 weeks' '+%a %b %d %H:%M:%S %Y'", HLVER.date));

    if (m_bVerbose && bShallow)
        progress.printMessageAbove(verboseString("will shallow since: {}", SHALLOW_DATE));

    std::string ret = execAndGet(std::format("cd {} && git clone --recursive '{}' hyprland-{}{}", getTempRoot(), HL_URL, USERNAME,
                                             (bShallow ? std::format(" --shallow-since='{}'", SHALLOW_DATE) : std::string{})));

    if (!std::filesystem::exists(WORKINGDIR)) {
        progress.printMessageAbove(failureString("Clone failed. Retrying without shallow."));
        ret = execAndGet(std::format("cd {} && git clone --recursive '{}' hyprland-{}", getTempRoot(), HL_URL, USERNAME));
    }

    if (!std::filesystem::exists(std::format("{}/.git", WORKINGDIR))) {
        std::println(stderr, "\n{}", failureString("Could not clone the Hyprland repository. shell returned:\n{}", ret));
        return false;
    }

    progress.printMessageAbove(successString("Hyprland cloned"));
    progress.m_iSteps           = 2;
    progress.m_szCurrentMessage = "Checking out sources";
    progress.print();

    if (m_bVerbose)
        progress.printMessageAbove(verboseString("will run: cd {} && git checkout {} 2>&1", WORKINGDIR, HLVER.hash));

    ret = execAndGet(std::format("cd {} && git checkout {} 2>&1", WORKINGDIR, HLVER.hash));

    if (ret.contains("fatal: unable to read tree")) {
        std::println(stderr, "\n{}",
                     failureString("Could not checkout the running Hyprland commit. If you are on -git, try updating.\n"
                                   "You can also try re-running hyprpm update with --no-shallow."));
        return false;
    }

    if (m_bVerbose)
        progress.printMessageAbove(verboseString("git returned (co): {}", ret));

    ret = execAndGet(std::format("cd {} ; git rm subprojects/tracy ; git submodule update --init 2>&1 ; git reset --hard --recurse-submodules {}", WORKINGDIR, HLVER.hash));

    if (m_bVerbose)
        progress.printMessageAbove(verboseString("git returned (rs): {}", ret));

    progress.printMessageAbove(successString("checked out to running ver"));
    progress.m_iSteps           = 3;
    progress.m_szCurrentMessage = "Building Hyprland";
    progress.print();

    progress.printMessageAbove(statusString("!", Colors::YELLOW, "configuring Hyprland"));

    if (m_bVerbose)
        progress.printMessageAbove(verboseString("setting PREFIX for cmake to {}", DataState::getHeadersPath()));

    const auto CONFIGURE_CMD =
        nixDevelopIfNeeded(std::format("cd {} && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:STRING=\"{}\" -S . -B ./build", WORKINGDIR,
                                       DataState::getHeadersPath()),
                           HLVER);

    if (!CONFIGURE_CMD) {
        std::println(stderr, "\n{}", failureString("Could not configure hyprland: {}", CONFIGURE_CMD.error()));
        return false;
    }

    ret = execAndGet(*CONFIGURE_CMD);
    if (m_bVerbose)
        progress.printMessageAbove(verboseString("cmake returned: {}", ret));

    if (ret.contains("CMake Error at")) {
        // missing deps, let the user know.
        std::string missing = ret.substr(ret.find("CMake Error at"));
        if (missing.contains('\n'))
            missing.erase(0, missing.find_first_of('\n') + 1);
        if (missing.contains("-- Configuring incomplete"))
            missing.erase(missing.find("-- Configuring incomplete"));
        if (missing.contains('\n'))
            missing.erase(missing.find_last_of('\n'));

        std::println(stderr, "\n{}",
                     failureString("Could not configure the hyprland source, cmake complained:\n{}\n\n"
                                   "This likely means that you are missing the above dependencies or they are out of date.",
                                   missing));
        return false;
    }

    progress.printMessageAbove(successString("configured Hyprland"));
    progress.m_iSteps           = 4;
    progress.m_szCurrentMessage = "Installing sources";
    progress.print();

    std::string cmd = std::format("sed -i -e \"s#PREFIX = /usr/local#PREFIX = {}#\" {}/Makefile", DataState::getHeadersPath(), WORKINGDIR);
    if (m_bVerbose)
        progress.printMessageAbove(verboseString("prepare install will run: {}", cmd));

    ret = execAndGet(cmd);

    cmd = std::format("make -C '{}' installheaders && chmod -R 644 '{}' && find '{}' -type d -exec chmod a+x {{}} \\;", WORKINGDIR, DataState::getHeadersPath(),
                      DataState::getHeadersPath());

    if (m_bVerbose)
        progress.printMessageAbove(verboseString("install will run as sudo: {}", cmd));

    // WORKINGDIR and headersPath should not contain anything unsafe. Usernames can't contain cmd exec parts.
    ret = NSys::root::runAsSuperuserUnsafe(cmd);

    if (m_bVerbose)
        std::println("{}", verboseString("installer returned: {}", ret));

    // remove build files
    std::filesystem::remove_all(WORKINGDIR);

    auto HEADERSVALID = headersValid();

    if (HEADERSVALID == HEADERS_OK || HEADERSVALID == HEADERS_ABI_MISMATCH) {
        progress.printMessageAbove(successString("installed headers"));
        progress.m_iSteps           = 5;
        progress.m_szCurrentMessage = "Done!";
        progress.print();

        std::print("\n");
    } else {
        progress.printMessageAbove(failureString("failed to install headers with error code {} ({})", sc<int>(HEADERSVALID), headerErrorShort(HEADERSVALID)));
        progress.printMessageAbove(infoString("if the problem persists, try running hyprpm purge-cache."));
        progress.m_iSteps           = 5;
        progress.m_szCurrentMessage = "Failed";
        progress.print();

        std::print(stderr, "\n\n{}", headerError(HEADERSVALID));

        return false;
    }

    return true;
}

bool CPluginManager::updatePlugins(bool forceUpdateAll) {
    const auto HEADERSVALID = headersValid();

    if (HEADERSVALID != HEADERS_OK && HEADERSVALID != HEADERS_ABI_MISMATCH) {
        std::println("{}", failureString("headers are not up-to-date, please run hyprpm update."));
        return false;
    }

    const auto HLVER = getHyprlandVersion(false);
    const auto REPOS = DataState::getAllRepositories();

    if (REPOS.size() < 1) {
        auto GLOBALSTATE               = DataState::getGlobalState();
        GLOBALSTATE.headersAbiCompiled = HLVER.abiHash;
        DataState::updateGlobalState(GLOBALSTATE);
        std::println("{}", failureString("No repos to update."));
        return true;
    }

    CProgressBar progress;
    progress.m_iMaxSteps        = (REPOS.size() * 2) + 2;
    progress.m_iSteps           = 0;
    progress.m_szCurrentMessage = "Updating repositories";
    progress.print();

    const std::string USERNAME = getpwuid(getuid())->pw_name;
    m_szWorkingPluginDirectory = std::format("{}{}", getTempRoot(), USERNAME);

    std::vector<std::string> failedRepos;

    const auto               markRepoFailed = [&](const SPluginRepository& repo, bool advanceProgress) {
        failedRepos.emplace_back(repo.name);
        std::filesystem::remove_all(m_szWorkingPluginDirectory);

        if (advanceProgress) {
            progress.m_iSteps++;
            progress.print();
        }
    };

    for (auto const& repo : REPOS) {
        bool update = forceUpdateAll;

        progress.m_iSteps++;
        progress.m_szCurrentMessage = std::format("Updating {}", repo.name);
        progress.print();

        progress.printMessageAbove(infoString("checking for updates for {}", repo.name));

        createSafeDirectory(m_szWorkingPluginDirectory);

        progress.printMessageAbove(infoString("Cloning {}", repo.url));

        std::string ret = execAndGet(std::format("cd {} && git clone --recursive '{}' {}", getTempRoot(), repo.url, USERNAME));

        if (!std::filesystem::exists(std::format("{}/.git", m_szWorkingPluginDirectory))) {
            std::println(stderr, "\n{}", failureString("could not clone repo: shell returned: {}", ret));
            markRepoFailed(repo, true);
            continue;
        }

        if (!repo.rev.empty()) {
            progress.printMessageAbove(infoString("Plugin has revision set, resetting: {}", repo.rev));

            std::string ret = execAndGet(std::format("git -C {} reset --hard --recurse-submodules \'{}\'", m_szWorkingPluginDirectory, repo.rev));
            if (ret.compare(0, 6, "fatal:") == 0) {
                std::println(stderr, "\n{}", failureString("could not check out revision {}: shell returned:\n{}", repo.rev, ret));

                markRepoFailed(repo, true);
                continue;
            }
        }

        if (!update) {
            // check if git has updates
            std::string hash = execAndGet(std::format("cd {} && git rev-parse HEAD", m_szWorkingPluginDirectory));
            if (!hash.empty())
                hash.pop_back();

            update = update || hash != repo.hash;
        }

        if (!update) {
            std::filesystem::remove_all(m_szWorkingPluginDirectory);
            progress.printMessageAbove(successString("repository {} is up-to-date.", repo.name));
            progress.m_iSteps++;
            progress.print();
            continue;
        }

        // we need to update

        progress.printMessageAbove(successString("repository {} has updates.", repo.name));
        progress.printMessageAbove(infoString("Building {}", repo.name));
        progress.m_iSteps++;
        progress.print();

        std::unique_ptr<CManifest> pManifest;

        if (std::filesystem::exists(std::format("{}/hyprpm.toml", m_szWorkingPluginDirectory))) {
            progress.printMessageAbove(successString("found hyprpm manifest"));
            pManifest = std::make_unique<CManifest>(MANIFEST_HYPRPM, std::format("{}/hyprpm.toml", m_szWorkingPluginDirectory));
        } else if (std::filesystem::exists(std::format("{}/hyprload.toml", m_szWorkingPluginDirectory))) {
            progress.printMessageAbove(successString("found hyprload manifest"));
            pManifest = std::make_unique<CManifest>(MANIFEST_HYPRLOAD, std::format("{}/hyprload.toml", m_szWorkingPluginDirectory));
        }

        if (!pManifest) {
            std::println(stderr, "\n{}", failureString("The provided plugin repository does not have a valid manifest"));
            markRepoFailed(repo, false);
            continue;
        }

        if (!pManifest->m_good) {
            std::println(stderr, "\n{}", failureString("The provided plugin repository has a bad manifest"));
            markRepoFailed(repo, false);
            continue;
        }

        if (repo.rev.empty() && !pManifest->m_repository.commitPins.empty()) {
            // check commit pins unless a revision is specified

            progress.printMessageAbove(infoString("Manifest has {} pins, checking", pManifest->m_repository.commitPins.size()));

            bool commitPinFailed = false;

            for (auto const& [hl, plugin] : pManifest->m_repository.commitPins) {
                if (hl != HLVER.hash)
                    continue;

                if (plugin.contains("'") || !isValidHash(plugin)) {
                    std::println(stderr, "\n{}", failureString("Plugin has a malformed manifest: bad commit pin"));
                    markRepoFailed(repo, false);
                    commitPinFailed = true;
                    break;
                }

                progress.printMessageAbove(successString("commit pin {} matched hl, resetting", plugin));

                execAndGet(std::format("cd {} && git reset --hard --recurse-submodules '{}'", m_szWorkingPluginDirectory, plugin));
            }

            if (commitPinFailed)
                continue;
        }

        bool anyPluginFailedToBuild = false;
        bool anyPluginSkipped       = false;

        for (auto& p : pManifest->m_plugins) {
            std::string out;

            if (p.since > HLVER.commits && HLVER.commits >= 1000 /* for shallow clones, we can't check this. 1000 is an arbitrary number I chose. */) {
                progress.printMessageAbove(failureString("Not building {}: your Hyprland version is too old.\n", p.name));
                p.failed         = true;
                anyPluginSkipped = true;
                continue;
            }

            progress.printMessageAbove(infoString("Building {}", p.name));

            const auto env = getPluginBuildEnv();

            for (auto const& bs : p.buildSteps) {
                const auto CMD_RAW = nixDevelopIfNeeded(std::format("cd {} && {} {}", m_szWorkingPluginDirectory, env, bs), HLVER);

                if (!CMD_RAW) {
                    progress.printMessageAbove(failureString("Failed to build {}: {}", p.name, CMD_RAW.error()));
                    break;
                }

                out += std::format(" -> {}\n{}\n", *CMD_RAW, execAndGet(*CMD_RAW));
            }

            if (m_bVerbose)
                std::println("{}", verboseString("shell returned: {}", out));

            if (!std::filesystem::exists(std::format("{}/{}", m_szWorkingPluginDirectory, p.output))) {
                std::println(stderr,
                             "\n{}\n"
                             "  This likely means that the plugin is either outdated, not yet available for your version, or broken.\n"
                             "If you are on -git, update first.\n"
                             "Try re-running with -v to see more verbose output.",
                             failureString("Plugin {} failed to build.", p.name));
                p.failed               = true;
                anyPluginFailedToBuild = true;
                continue;
            }

            progress.printMessageAbove(successString("built {} into {}", p.name, p.output));
        }

        // add repo toml to DataState
        SPluginRepository newrepo = repo;
        newrepo.plugins.clear();
        execAndGet(std::format("cd {} && git pull --recurse-submodules && git reset --hard --recurse-submodules",
                               m_szWorkingPluginDirectory)); // repo hash in the state.toml has to match head and not any pin
        std::string repohash = execAndGet(std::format("cd {} && git rev-parse HEAD", m_szWorkingPluginDirectory));
        if (!repohash.empty())
            repohash.pop_back();
        // a build failure must not record the fetched hash: the next update would consider the
        // repo up-to-date and never retry the build. An empty hash never matches, so it retries.
        newrepo.hash = anyPluginFailedToBuild ? "" : repohash;
        for (auto const& p : pManifest->m_plugins) {
            const auto OLDPLUGINIT = std::ranges::find_if(repo.plugins, [&](const auto& other) { return other.name == p.name; });
            newrepo.plugins.emplace_back(SPlugin{.name     = p.name,
                                                 .filename = std::format("{}/{}", m_szWorkingPluginDirectory, p.output),
                                                 .enabled  = OLDPLUGINIT != repo.plugins.end() ? OLDPLUGINIT->enabled : false,
                                                 .failed   = p.failed});
        }
        DataState::removePluginRepo(SPluginRepoIdentifier::fromName(newrepo.name));
        DataState::addNewPluginRepo(newrepo);

        std::filesystem::remove_all(m_szWorkingPluginDirectory);

        if (anyPluginFailedToBuild) {
            failedRepos.emplace_back(repo.name);
            progress.printMessageAbove(failureString("updated {}, but some plugins failed to build", repo.name));
            continue;
        }

        if (anyPluginSkipped) {
            // skipping is a persistent condition (hyprland too old for the plugin), so it deliberately
            // does not fail the update: that would force a full rebuild on every subsequent update
            progress.printMessageAbove(statusString("!", Colors::YELLOW, "updated {}, but some plugins were skipped: they require a newer Hyprland", repo.name));
            continue;
        }

        progress.printMessageAbove(successString("updated {}", repo.name));
    }

    progress.m_iSteps++;
    progress.m_szCurrentMessage = "Updating global state...";
    progress.print();

    if (failedRepos.empty()) {
        auto GLOBALSTATE               = DataState::getGlobalState();
        GLOBALSTATE.headersAbiCompiled = HLVER.abiHash;
        DataState::updateGlobalState(GLOBALSTATE);
    }

    progress.m_iSteps++;
    progress.m_szCurrentMessage = failedRepos.empty() ? "Done!" : "Done with errors";
    progress.print();

    std::print("\n");

    if (!failedRepos.empty()) {
        std::string failedRepoNames;
        for (const auto& name : failedRepos) {
            if (!failedRepoNames.empty())
                failedRepoNames += ", ";
            failedRepoNames += name;
        }

        std::println(stderr, "{}", failureString("failed to update the following repositories: {}", failedRepoNames));
    }

    return failedRepos.empty();
}

bool CPluginManager::enablePlugin(const SPluginRepoIdentifier& identifier) {
    bool ret = false;

    switch (identifier.type) {
        case IDENTIFIER_NAME:
        case IDENTIFIER_AUTHOR_NAME: ret = DataState::setPluginEnabled(identifier, true); break;
        default: return false;
    }
    if (ret)
        std::println("{}", successString("Enabled {}", identifier.name));
    return ret;
}

bool CPluginManager::disablePlugin(const SPluginRepoIdentifier& identifier) {
    bool ret = DataState::setPluginEnabled(identifier, false);
    if (ret)
        std::println("{}", successString("Disabled {}", identifier.name));
    return ret;
}

ePluginLoadStateReturn CPluginManager::ensurePluginsLoadState(bool forceReload) {
    if (headersValid() != HEADERS_OK) {
        std::println(stderr, "\n{}", failureString("headers are not up-to-date, please run hyprpm update."));
        return LOADSTATE_HEADERS_OUTDATED;
    }

    const auto HOME = getenv("HOME");
    const auto HIS  = getenv("HYPRLAND_INSTANCE_SIGNATURE");
    if (!HOME || !HIS) {
        std::println(stderr, "PluginManager: no $HOME or $HYPRLAND_INSTANCE_SIGNATURE");
        return LOADSTATE_FAIL;
    }
    const auto HYPRPMPATH = DataState::getDataStatePath();

    const auto json = glz::read_json<glz::generic::array_t>(NHyprlandSocket::send("j/plugins list"));
    if (!json) {
        std::println(stderr, "PluginManager: couldn't parse plugin list output");
        return LOADSTATE_FAIL;
    }

    std::vector<std::string> loadedPlugins;
    for (const auto& plugin : json.value()) {
        if (!plugin.is_object() || !plugin.contains("name")) {
            std::println(stderr, "PluginManager: couldn't parse plugin object");
            return LOADSTATE_FAIL;
        }
        loadedPlugins.emplace_back(plugin["name"].get<std::string>());
    }

    std::println("{}", successString("Ensuring plugin load state"));

    // get state
    const auto REPOS = DataState::getAllRepositories();

    auto       enabled = [REPOS](const std::string& plugin) -> bool {
        for (auto const& r : REPOS) {
            for (auto const& p : r.plugins) {
                if (p.name == plugin && p.enabled)
                    return true;
            }
        }

        return false;
    };

    auto repoForName = [REPOS](const std::string& name) -> std::string {
        for (auto const& r : REPOS) {
            for (auto const& p : r.plugins) {
                if (p.name == name)
                    return r.name;
            }
        }

        return "";
    };

    // if any of the loadUnloadPlugin calls return false, this is true
    // bcs that means the header version doesn't match the running version
    // (and Hyprland needs to restart)
    bool hyprlandVersionMismatch = false;

    // unload disabled plugins (or all if forceReload is true)
    for (auto const& p : loadedPlugins) {
        if (forceReload || !enabled(p)) {
            // unload
            if (!loadUnloadPlugin(HYPRPMPATH / repoForName(p) / std::format("{}.so", p), false)) {
                std::println("{}", infoString("{} will be unloaded after restarting Hyprland", p));
                hyprlandVersionMismatch = true;
            } else
                std::println("{}", successString("Unloaded {}", p));
        }
    }

    // load enabled plugins
    for (auto const& r : REPOS) {
        for (auto const& p : r.plugins) {
            if (!p.enabled)
                continue;

            if (p.failed) {
                std::println("{}", infoString("Not loading {}: it was not built", p.name));
                continue;
            }

            if (!forceReload && std::ranges::find_if(loadedPlugins, [&](const auto& other) { return other == p.name; }) != loadedPlugins.end())
                continue;

            if (!loadUnloadPlugin(HYPRPMPATH / repoForName(p.name) / p.filename, true)) {
                std::println("{}", infoString("{} will be loaded after restarting Hyprland", p.name));
                hyprlandVersionMismatch = true;
            } else
                std::println("{}", successString("Loaded {}", p.name));
        }
    }

    std::println("{}", successString("Plugin load state ensured"));

    return hyprlandVersionMismatch ? LOADSTATE_HYPRLAND_UPDATED : LOADSTATE_OK;
}

bool CPluginManager::loadUnloadPlugin(const std::string& path, bool load) {
    auto state = DataState::getGlobalState();
    auto HLVER = getHyprlandVersion(true);

    if (state.headersAbiCompiled != HLVER.abiHash) {
        if (load)
            std::println("{}", infoString("Running Hyprland version ({}) differs from plugin state ({}), please restart Hyprland.", HLVER.hash, state.headersAbiCompiled));
        return false;
    }

    if (load)
        NHyprlandSocket::send(std::format("/plugin load {}", path));
    else
        NHyprlandSocket::send(std::format("/plugin unload {}", path));

    return true;
}

void CPluginManager::listAllPlugins() {
    const auto REPOS = DataState::getAllRepositories();

    for (auto const& r : REPOS) {
        std::println("{}", infoString("Repository {} (by {}):", r.name, r.author));

        for (auto const& p : r.plugins) {
            std::println("  │ Plugin {}", p.name);

            if (!p.failed)
                std::println("  └─ enabled: {}", (p.enabled ? std::format("{}true", Colors::GREEN) : std::format("{}false", Colors::RED)));
            else
                std::println("  └─ enabled: {}Plugin failed to build", Colors::RED);

            std::println("{}", Colors::RESET);
        }
    }
}

void CPluginManager::notify(const eNotifyIcons icon, uint32_t color, int durationMs, const std::string& message) {
    NHyprlandSocket::send(std::format("/notify {} {} {} {}", sc<int>(icon), durationMs, color, message));
}

std::string CPluginManager::headerError(const eHeadersErrors err) {
    switch (err) {
        case HEADERS_CORRUPTED: return failureString("Headers corrupted. Please run hyprpm update to fix those.\n");
        case HEADERS_MISMATCHED: return failureString("Headers version mismatch. Please run hyprpm update to fix those.\n");
        case HEADERS_NOT_HYPRLAND: return failureString("It doesn't seem you are running on hyprland.\n");
        case HEADERS_MISSING: return failureString("Headers missing. Please run hyprpm update to fix those.\n");
        case HEADERS_ABI_MISMATCH: return failureString("ABI is mismatched. Please run hyprpm update to fix that.\n");
        case HEADERS_DUPLICATED: {
            return failureString("Headers duplicated!!! This is a very bad sign.\n"
                                 "This could be due to e.g. installing hyprland manually while a system package of hyprland is also installed.\n"
                                 "If the above doesn't apply, check your /usr/include and /usr/local/include directories\n and remove all the hyprland headers.\n");
        }
        default: break;
    }

    return failureString("Unknown header error. Please run hyprpm update to fix those.\n");
}

std::string CPluginManager::headerErrorShort(const eHeadersErrors err) {
    switch (err) {
        case HEADERS_CORRUPTED: return "Headers corrupted";
        case HEADERS_MISMATCHED: return "Headers version mismatched";
        case HEADERS_NOT_HYPRLAND: return "Not running on Hyprland";
        case HEADERS_MISSING: return "Headers missing";
        case HEADERS_DUPLICATED: return "Headers duplicated";
        default: break;
    }
    return "?";
}

bool CPluginManager::hasDeps() {
    if (!m_bNoNix && getHyprlandVersion().isNix)
        return true; // dep check not needed if we are on nix

    bool                     hasAllDeps = true;
    std::vector<std::string> deps       = {"cpio", "cmake", "pkg-config", "g++", "gcc", "git"};

    for (auto const& d : deps) {
        if (!execAndGet(std::format("command -v {}", d)).contains("/")) {
            std::println(stderr, "{}", failureString("Missing dependency: {}", d));
            hasAllDeps = false;
        }
    }

    return hasAllDeps;
}

std::string CPluginManager::getPluginBuildEnv() {
    std::string env = std::format("PKG_CONFIG_PATH=\"{}\"", getPkgConfigPath());

#if defined(HYPRPM_EXTRA_CFLAGS)
    if (std::string_view{HYPRPM_EXTRA_CFLAGS}.size() > 0)
        env += std::format(" CFLAGS=\"{} $CFLAGS\"", HYPRPM_EXTRA_CFLAGS);
#endif

#if defined(HYPRPM_EXTRA_CXXFLAGS)
    if (std::string_view{HYPRPM_EXTRA_CXXFLAGS}.size() > 0)
        env += std::format(" CXXFLAGS=\"{} $CXXFLAGS\"", HYPRPM_EXTRA_CXXFLAGS);
#endif

    return env;
}

const std::string& CPluginManager::getPkgConfigPath() {
    static const auto str = std::format("{}/share/pkgconfig:$PKG_CONFIG_PATH", DataState::getHeadersPath());
    return str;
}

static std::expected<std::string, std::string> getNixDevelopFromPath(const std::string& argv0) {
    std::string fullStorePath;

    if (argv0.starts_with("/")) {
        // we can use this directly
        fullStorePath = argv0;
    } else {
        // use hyprpm, find in path
        auto exe = NSys::findInPath("hyprpm");
        if (!exe)
            return std::unexpected("hyprpm not found in PATH");

        fullStorePath = *exe;
    }

    if (fullStorePath.empty() || !fullStorePath.ends_with("/bin/hyprpm"))
        return std::unexpected("couldn't get a real path for hyprpm (1)");

    // canonicalize to get the real nix-store path
    std::error_code ec;
    fullStorePath = std::filesystem::canonical(fullStorePath, ec);

    if (ec || fullStorePath.empty() || !fullStorePath.starts_with("/nix"))
        return std::unexpected("couldn't get a real path for hyprpm");

    fullStorePath.resize(fullStorePath.length() - std::string_view{"/bin/hyprpm"}.length());

    auto deriver = trim(execAndGet(std::format("echo \"$(nix-store --query --deriver '{}')\"", fullStorePath)));

    if (deriver.starts_with("unknown"))
        return std::unexpected("couldn't nix deriver");

    return deriver;
}

static std::expected<std::string, std::string> getNixDevelopFromProfile() {
    const auto NIX_PROFILE_STR = execAndGet("nix profile list --json");

    auto       rawJson = glz::read_json<glz::generic>(NIX_PROFILE_STR);

    if (!rawJson)
        return std::unexpected("failed to parse nix profile list --json");

    auto& json = *rawJson;

    if (!json.contains("elements") || !json["elements"].is_object())
        return std::unexpected("nix profile list --json returned a wonky json");

    if (!json["elements"].contains("hyprland") && !json["elements"].contains("Hyprland"))
        return std::unexpected("nix profile list --json doesn't contain Hyprland (did you uninstall?)");

    auto& hyprlandJson = json["elements"].contains("hyprland") ? json["elements"]["hyprland"] : json["elements"]["Hyprland"];

    if (!hyprlandJson.contains("originalUrl"))
        return std::unexpected("nix profile list --json's hyprland doesn't contain originalUrl?");

    return hyprlandJson["originalUrl"].get_string();
}

std::expected<std::string, std::string> CPluginManager::nixDevelopIfNeeded(const std::string& cmd, const SHyprlandVersion& ver) {
    if (m_bNoNix || !ver.isNix)
        return cmd;

    // Escape single quotes
    std::string newCmd = cmd;
    replaceInString(newCmd, "'", "\\'");

    auto NIX_DEVELOP = getNixDevelopFromPath(m_szArgv0);

    if (NIX_DEVELOP)
        return std::format("nix develop '{}' --command bash -c $'{}'", *NIX_DEVELOP, newCmd);
    else if (m_bVerbose)
        std::println("{}", verboseString("Failed nix from path: {}", NIX_DEVELOP.error()));

    NIX_DEVELOP = getNixDevelopFromProfile();

    if (NIX_DEVELOP)
        return std::format("nix develop '{}' --command bash -c $'{}'", *NIX_DEVELOP, newCmd);
    else if (m_bVerbose)
        std::println("{}", verboseString("Failed nix from profile: {}", NIX_DEVELOP.error()));

    return std::unexpected("hyprland is nix, but hyprpm failed to obtain a nix develop shell for build cmd");
}
