#include "LuaBindingsInternal.hpp"
#include "Check.hpp"

#include "../objects/LuaEventSubscription.hpp"
#include "../objects/LuaKeybind.hpp"
#include "../objects/LuaTimer.hpp"

#include "../../supplementary/executor/Executor.hpp"

#include "../../../devices/IKeyboard.hpp"
#include "../../../managers/eventLoop/EventLoopManager.hpp"
#include "../../../managers/SessionLockManager.hpp"
#include "../../../plugins/PluginSystem.hpp"
#include "managers/KeybindManager.hpp"

#include <hyprutils/string/Numeric.hpp>
#include <hyprutils/string/String.hpp>
#include <hyprutils/string/VarList.hpp>

using namespace Config;
using namespace Config::Lua;
using namespace Config::Lua::Bindings;
using namespace Hyprutils::String;

extern "C" {
#include <lua.h>
#include <xkbcommon/xkbcommon.h>
}

static std::optional<eKeyboardModifiers> modFromSv(std::string_view sv) {
    if (sv == "SHIFT")
        return HL_MODIFIER_SHIFT;
    if (sv == "CAPS")
        return HL_MODIFIER_CAPS;
    if (sv == "CTRL" || sv == "CONTROL")
        return HL_MODIFIER_CTRL;
    if (sv == "ALT" || sv == "MOD1")
        return HL_MODIFIER_ALT;
    if (sv == "MOD2")
        return HL_MODIFIER_MOD2;
    if (sv == "MOD3")
        return HL_MODIFIER_MOD3;
    if (sv == "SUPER" || sv == "WIN" || sv == "LOGO" || sv == "MOD4" || sv == "META")
        return HL_MODIFIER_META;
    if (sv == "MOD5")
        return HL_MODIFIER_MOD5;

    return std::nullopt;
}

static bool isSymSpecial(std::string_view sv) {
    if (sv == "mouse_down" || sv == "mouse_up" || sv == "mouse_left" || sv == "mouse_right")
        return true;

    return sv.starts_with("switch:") || sv.starts_with("mouse:");
}

static std::expected<void, std::string> parseKeyString(SKeybind& kb, std::string_view sv) {
    bool                                                modsEnded = false, specialSym = false;
    CVarList2                                           vl(sv, 0, '+', true);

    uint32_t                                            modMask = 0;
    std::vector<std::pair<xkb_keysym_t, xkb_keycode_t>> keysyms;
    std::string                                         lastKeyArg;

    if (sv == "catchall") {
        kb.catchAll = true;
        return {};
    }

    for (const auto& a : vl) {
        auto arg = Hyprutils::String::trim(a);

        auto mask = modFromSv(arg);

        if (!mask)
            modsEnded = true;

        if (modsEnded && mask)
            return std::unexpected("Modifiers must come first in the list");

        if (mask) {
            modMask |= *mask;
            continue;
        }

        if (specialSym)
            return std::unexpected("Cannot combine special syms (e.g. mouse_down + Q)");

        if (isSymSpecial(arg)) {
            if (!keysyms.empty())
                return std::unexpected("Cannot combine special syms (e.g. mouse_down + Q)");

            specialSym = true;
            kb.key     = arg;
            continue;
        }

        if (arg.starts_with("code:") && isNumber(std::string{arg.substr(5)})) {
            auto res = strToNumber<uint32_t>(arg.substr(5));

            if (!res)
                return std::unexpected(std::format("Invalid keycode: \"{}\".", arg));

            keysyms.emplace_back(XKB_KEY_NoSymbol, xkb_keycode_t{*res});
            continue;
        }

        auto sym = xkb_keysym_from_name(std::string{arg}.c_str(), XKB_KEYSYM_CASE_INSENSITIVE);

        if (sym == XKB_KEY_NoSymbol) {
            if (arg.contains(' '))
                return std::unexpected(std::format("Unknown keysym: \"{}\", did you forget a +?", arg));

            if (arg == "Enter")
                return std::unexpected(std::format(R"(Unknown keysym: "{}", did you mean "Return"?)", arg));

            return std::unexpected(std::format("Unknown keysym: \"{}\"", arg));
        }

        lastKeyArg = arg;
        keysyms.emplace_back(sym, 0);
    }

    kb.modmask = modMask;
    kb.sMkKeys = std::move(keysyms);
    if (!specialSym && !lastKeyArg.empty())
        kb.key = lastKeyArg;
    return {};
}

static int hlBind(lua_State* L) {
    auto* mgr = sc<CConfigManager*>(lua_touserdata(L, lua_upvalueindex(1)));

    auto  str = Check::string(L, 1);
    if (!str)
        return Internal::configError(L, std::format("bind: bad argument 1: {}", str.error()));

    std::string_view keys = *str;

    SKeybind         kb;
    kb.submap.name  = mgr->m_currentSubmap;
    kb.submap.reset = mgr->m_currentSubmapReset;

    if (auto res = parseKeyString(kb, keys); !res)
        return Internal::configError(L, std::format("hl.bind: failed to parse key string: {}", res.error()));

    if (!Internal::pushDispatcherFunction(L, 2))
        return Internal::configError(L, "hl.bind: dispatcher must be a dispatcher (e.g. hl.dsp.window.close()) or a lua function");

    if (kb.catchAll && mgr->m_currentSubmap.empty())
        return Internal::configError(L, "hl.bind: catchall keybinds are only allowed in submaps.");

    int ref       = luaL_ref(L, LUA_REGISTRYINDEX);
    kb.handler    = "__lua";
    kb.arg        = std::to_string(ref);
    kb.displayKey = keys;

    int optsIdx = 3;

    if (lua_istable(L, optsIdx)) {
        auto getBool = [&](const char* field) -> bool {
            lua_getfield(L, optsIdx, field);
            const bool v = lua_toboolean(L, -1);
            lua_pop(L, 1);
            return v;
        };

        auto readOptString = [&](const char* field) -> std::optional<std::string> {
            lua_getfield(L, optsIdx, field);

            if (lua_isnil(L, -1)) {
                lua_pop(L, 1);
                return std::nullopt;
            }

            if (!lua_isstring(L, -1)) {
                lua_pop(L, 1);
                Internal::configError(L, "hl.bind: opts.{} must be a string", field);
                return std::nullopt;
            }

            std::string result = lua_tostring(L, -1);
            lua_pop(L, 1);
            return result;
        };

        kb.repeat          = getBool("repeating");
        kb.locked          = getBool("locked");
        kb.release         = getBool("release");
        kb.nonConsuming    = getBool("non_consuming");
        kb.autoConsuming   = getBool("auto_consuming");
        kb.transparent     = getBool("transparent");
        kb.ignoreMods      = getBool("ignore_mods");
        kb.dontInhibit     = getBool("dont_inhibit");
        kb.longPress       = getBool("long_press");
        kb.submapUniversal = getBool("submap_universal");

        if (auto description = readOptString("description"); description.has_value()) {
            kb.description    = *description;
            kb.hasDescription = true;
        } else if (auto desc = readOptString("desc"); desc.has_value()) {
            kb.description    = *desc;
            kb.hasDescription = true;
        }

        bool click = false;
        bool drag  = false;

        if (getBool("click")) {
            click      = true;
            kb.release = true;
        }

        if (getBool("drag")) {
            drag       = true;
            kb.release = true;
        }

        if (click && drag)
            return Internal::configError(L, "hl.bind: click and drag are exclusive");

        if ((kb.longPress || kb.release) && kb.repeat)
            return Internal::configError(L, "hl.bind: long_press / release is incompatible with repeat");

        if (kb.mouse && (kb.repeat || kb.release || kb.locked))
            return Internal::configError(L, "hl.bind: mouse is exclusive");

        kb.click = click;
        kb.drag  = drag;

        lua_getfield(L, optsIdx, "device");
        if (lua_istable(L, -1)) {
            lua_getfield(L, -1, "inclusive");
            kb.deviceInclusive = lua_isnil(L, -1) ? true : lua_toboolean(L, -1);
            lua_pop(L, 1);

            lua_getfield(L, -1, "list");
            if (lua_istable(L, -1)) {
                lua_pushnil(L);
                while (lua_next(L, -2)) {
                    if (lua_isstring(L, -1))
                        kb.devices.emplace(lua_tostring(L, -1));
                    lua_pop(L, 1);
                }
            }
            lua_pop(L, 1);
        }
        kb.allowInputCapture = getBool("allow_input_capture");
        lua_pop(L, 1);
    }

    const auto BIND = g_pKeybindManager->addKeybind(kb);
    Objects::CLuaKeybind::push(L, BIND);
    return 1;
}

static int hlDefineSubmap(lua_State* L) {
    auto* mgr  = sc<CConfigManager*>(lua_touserdata(L, lua_upvalueindex(1)));
    auto  name = Check::string(L, 1);
    if (!name)
        return Internal::configError(L, std::format("define_submap: bad argument 1: {}", name.error()));

    std::string reset;
    int         fnIdx = 2;
    if (lua_gettop(L) >= 3 && lua_isstring(L, 2)) {
        reset = lua_tostring(L, 2);
        fnIdx = 3;
    }

    luaL_checktype(L, fnIdx, LUA_TFUNCTION);

    std::string prev          = mgr->m_currentSubmap;
    std::string prevReset     = mgr->m_currentSubmapReset;
    mgr->m_currentSubmap      = *name;
    mgr->m_currentSubmapReset = reset;

    lua_pushvalue(L, fnIdx);
    if (mgr->guardedPCall(0, 0, 0, CConfigManager::LUA_TIMEOUT_DISPATCH_MS, std::format("hl.define_submap(\"{}\")", *name)) != LUA_OK) {
        mgr->addError(std::format("hl.define_submap: error in submap \"{}\": {}", *name, lua_tostring(L, -1)));
        lua_pop(L, 1);
    }

    mgr->m_currentSubmap      = prev;
    mgr->m_currentSubmapReset = prevReset;
    return 0;
}

static int hlVersion(lua_State* L) {
    lua_pushstring(L, HYPRLAND_VERSION);
    return 1;
}

static int hlGetPlugins(lua_State* L) {
    if (!g_pPluginSystem) {
        lua_newtable(L);
        return 1;
    }

    const auto PLUGINS = g_pPluginSystem->getAllPlugins();

    lua_createtable(L, PLUGINS.size(), 0);

    int i = 1;
    for (const auto& plugin : PLUGINS) {
        lua_createtable(L, 0, 4);
        lua_pushstring(L, plugin->m_name.c_str());
        lua_setfield(L, -2, "name");
        lua_pushstring(L, plugin->m_author.c_str());
        lua_setfield(L, -2, "author");
        lua_pushstring(L, plugin->m_version.c_str());
        lua_setfield(L, -2, "version");
        lua_pushstring(L, plugin->m_description.c_str());
        lua_setfield(L, -2, "description");
        lua_rawseti(L, -2, i++);
    }

    return 1;
}

static int hlExecCmd(lua_State* L) {
    auto cmd = Internal::argStr(L, 1);

    if (cmd.empty())
        return Internal::configError(L, "hl.exec_cmd: expected command as first argument");

    auto rule = Internal::buildRuleFromTable(L, 2);

    if (!rule)
        return rule.error();

    Config::Supplementary::executor()->spawn(Supplementary::SExecRequest{cmd, !*rule, *rule});

    return 0;
}

static int hlClearCrashedLockscreen(lua_State* L) {
    if (!g_pSessionLockManager)
        return Internal::configError(L, "hl.clear_crashed_lockscreen: sessionLockMgr not init'd yet");

    if (!g_pSessionLockManager->isSessionLocked())
        return Internal::configError(L, "hl.clear_crashed_lockscreen: session is not locked");

    if (g_pSessionLockManager->clientLocked() || g_pSessionLockManager->clientDenied())
        return Internal::configError(L, "hl.clear_crashed_lockscreen: session is locked with a client, refusing to unlock");

    g_pSessionLockManager->forceUnlock();

    return 0;
}

static int hlDispatch(lua_State* L) {
    if (!Internal::pushDispatcherFunction(L, 1))
        return Internal::configError(L, "hl.dispatch: expected a dispatcher (e.g. hl.dsp.window.close())");

    int status = LUA_OK;
    if (auto* mgr = CConfigManager::fromLuaState(L); mgr)
        status = mgr->guardedPCall(0, 1, 0, CConfigManager::LUA_TIMEOUT_DISPATCH_MS, "hl.dispatch");
    else
        status = lua_pcall(L, 0, 1, 0);

    if (status != LUA_OK) {
        const char* err = lua_tostring(L, -1);
        lua_pop(L, 1);
        return Internal::dispatcherError(L, std::format("hl.dispatch: {}", err ? err : "unknown error"), Config::Actions::eActionErrorLevel::ERROR,
                                         Config::Actions::eActionErrorCode::LUA_ERROR);
    }

    if (lua_isnil(L, -1)) {
        lua_pop(L, 1);
        return Internal::pushSuccessResult(L);
    }

    return 1;
}

static int hlOn(lua_State* L) {
    auto* mgr    = sc<CConfigManager*>(lua_touserdata(L, lua_upvalueindex(1)));
    auto  evName = Check::string(L, 1);
    if (!evName)
        return Internal::configError(L, std::format("on: bad argument 1: {}", evName.error()));
    luaL_checktype(L, 2, LUA_TFUNCTION);

    lua_pushvalue(L, 2);
    int        ref = luaL_ref(L, LUA_REGISTRYINDEX);

    const auto handle = mgr->m_eventHandler->registerEvent(*evName, ref);
    if (!handle.has_value()) {
        luaL_unref(L, LUA_REGISTRYINDEX, ref);
        const auto& known = CLuaEventHandler::knownEvents();
        std::string list;
        for (const auto& e : known) {
            list += e + ", ";
        }
        list.pop_back();
        list.pop_back();
        return Internal::configError(L, "hl.on: unknown event \"{}\". Known events:{}", *evName, list);
    }

    Objects::CLuaEventSubscription::push(L, mgr->m_eventHandler.get(), *handle);
    return 1;
}

static int hlUnbind(lua_State* L) {
    if (lua_isstring(L, 1) && std::string_view(lua_tostring(L, 1)) == "all" && lua_gettop(L) == 1) {
        g_pKeybindManager->clearKeybinds();
        return 0;
    }

    auto str = Check::string(L, 1);
    if (!str)
        return Internal::configError(L, std::format("unbind: bad argument 1: {}", str.error()));
    g_pKeybindManager->removeKeybind(*str);

    return 0;
}

static int hlIsKeyDown(lua_State* L) {
    if (lua_isinteger(L, 1)) {
        // Confirm code is valid
        auto keycode = lua_tointeger(L, 1);
        if (!xkb_keycode_is_legal_x11(keycode) && !xkb_keycode_is_legal_ext(keycode))
            return Internal::configError(L, std::format("hl.is_key_down: invalid keycode {}", keycode));

        // Return whether it's pressed or not
        auto isKeyDown = false;
        for (auto& k : g_pKeybindManager->m_pressedKeys) {
            if (k.keycode == keycode) {
                isKeyDown = true;
                break;
            }
        }
        lua_pushboolean(L, isKeyDown);
        return 1;
    } else if (lua_isstring(L, 1)) {
        // Parse keysym
        auto key = std::string(lua_tostring(L, 1));
        auto sym = xkb_keysym_from_name(key.c_str(), XKB_KEYSYM_NO_FLAGS);
        if (sym == XKB_KEY_NoSymbol) {
            if (key == "Enter")
                return Internal::configError(L, std::format(R"(Unknown keysym: "{}", did you mean "Return"?)", key));

            return Internal::configError(L, std::format("Unknown keysym: \"{}\"", key));
        }

        // Return whether it's pressed or not
        auto isKeyDown = false;
        for (auto& k : g_pKeybindManager->m_pressedKeys) {
            if (k.keysym == sym) {
                isKeyDown = true;
                break;
            }
        }
        lua_pushboolean(L, isKeyDown);
        return 1;
    }
    return Internal::configError(L, std::format("hl.is_key_down: bad argument 1: expected integer or string"));
}

static int hlTimer(lua_State* L) {
    auto* mgr = sc<CConfigManager*>(lua_touserdata(L, lua_upvalueindex(1)));

    luaL_checktype(L, 1, LUA_TFUNCTION);
    luaL_checktype(L, 2, LUA_TTABLE);

    lua_getfield(L, 2, "timeout");
    if (!lua_isnumber(L, -1))
        return Internal::configError(L, "hl.timer: opts.timeout must be a number (ms)");
    int timeoutMs = (int)lua_tonumber(L, -1);
    lua_pop(L, 1);

    if (timeoutMs <= 0)
        return Internal::configError(L, "hl.timer: opts.timeout must be > 0");

    lua_getfield(L, 2, "type");
    if (!lua_isstring(L, -1))
        return Internal::configError(L, "hl.timer: opts.type must be \"repeat\" or \"oneshot\"");
    std::string type = lua_tostring(L, -1);
    lua_pop(L, 1);

    bool repeat = false;
    if (type == "repeat")
        repeat = true;
    else if (type != "oneshot")
        return Internal::configError(L, "hl.timer: opts.type must be \"repeat\" or \"oneshot\"");

    lua_pushvalue(L, 1);
    int  ref = luaL_ref(L, LUA_REGISTRYINDEX);

    auto timer = makeShared<CEventLoopTimer>(
        std::chrono::milliseconds(timeoutMs),
        [L, ref, repeat, timeoutMs, mgr](SP<CEventLoopTimer> self, void* data) {
            // update repeat already so that if we call set_timeout inside
            // our timer it doesn't get overwritten
            if (repeat)
                self->updateTimeout(std::chrono::milliseconds(timeoutMs));

            lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
            int status = LUA_OK;
            if (mgr)
                status = mgr->guardedPCall(0, 0, 0, CConfigManager::LUA_TIMEOUT_TIMER_CALLBACK_MS, "hl.timer callback");
            else
                status = lua_pcall(L, 0, 0, 0);

            if (status != LUA_OK) {
                Log::logger->log(Log::ERR, "[Lua] error in timer callback: {}", lua_tostring(L, -1));
                lua_pop(L, 1);
            }

            if (!repeat) {
                const auto HAS = std::ranges::find_if(mgr->m_luaTimers, [&self](const auto& lt) { return lt.timer == self; }) != mgr->m_luaTimers.end();

                // avoid double-unref if this timer triggered a reload
                if (HAS) {
                    luaL_unref(L, LUA_REGISTRYINDEX, ref);
                    std::erase_if(mgr->m_luaTimers, [&self](const auto& lt) { return lt.timer == self; });
                }
            }
        },
        nullptr);

    mgr->m_luaTimers.emplace_back(CConfigManager::SLuaTimer{timer, ref});
    if (g_pEventLoopManager)
        g_pEventLoopManager->addTimer(timer);

    Objects::CLuaTimer::push(L, timer, timeoutMs);
    return 1;
}

static int hlExecuteScheduledRefreshImmediately(lua_State* L) {

    return Supplementary::refresher()->executeScheduledRefreshImmediately();
}

void Internal::registerToplevelBindings(lua_State* L, CConfigManager* mgr) {
    Internal::setMgrFn(L, mgr, "on", hlOn);
    Internal::setMgrFn(L, mgr, "bind", hlBind);
    Internal::setMgrFn(L, mgr, "define_submap", hlDefineSubmap);
    Internal::setMgrFn(L, mgr, "timer", hlTimer);

    Internal::setFn(L, "dispatch", hlDispatch);
    Internal::setFn(L, "version", hlVersion);
    Internal::setFn(L, "get_loaded_plugins", hlGetPlugins);
    Internal::setFn(L, "exec_cmd", hlExecCmd);

    Internal::setFn(L, "clear_crashed_lockscreen", hlClearCrashedLockscreen);

    Internal::setFn(L, "exec_scheduled_prop_refresh_immediately", hlExecuteScheduledRefreshImmediately);

    Internal::setFn(L, "unbind", hlUnbind);

    Internal::setFn(L, "is_key_down", hlIsKeyDown);
}
