local api_flags_create = require("../Common/api_flags")
local runtime_create = require("../Common/runtime_create")
local vm_common_init = require("../VM/common")

local rt, memory_at_0, cfns, INDIRECT_FUNCTIONS, utils = runtime_create(require("../VM/polyfill"))
local api_flags = api_flags_create(rt, memory_at_0, cfns, INDIRECT_FUNCTIONS, utils)
local vm_common = vm_common_init(rt, memory_at_0, cfns, INDIRECT_FUNCTIONS, utils)

return {
    luau_run = vm_common.luau_run,
    luau_load = vm_common.luau_load,
    luau_setflag = api_flags.luau_setflag,
    luau_setallflags = api_flags.luau_setallflags,
    luau_resetflags = api_flags.luau_resetflags,
    macros = require("../VM/luau_macros")(rt, memory_at_0, cfns, INDIRECT_FUNCTIONS, utils),
    wasm = {
        rt = rt,
        cfns = cfns,
        indirect_function_table = INDIRECT_FUNCTIONS,
        memory = memory_at_0,
        insertCFunction = utils.insertCFunction,
        cstr = utils.cstr,
    }
}


local LUAI_MAXCSTACK    = 8000
local LUA_REGISTRYINDEX = -LUAI_MAXCSTACK - 2000
local LUA_ENVIRONINDEX  = -LUAI_MAXCSTACK - 2001
local LUA_GLOBALSINDEX  = -LUAI_MAXCSTACK - 2002
local LUA_VECTOR_SIZE   = 3

local LUA_TNONE          = -1
local LUA_TNIL           = 0
local LUA_TBOOLEAN       = 1
local LUA_TLIGHTUSERDATA = 2
local LUA_TNUMBER        = 3
local LUA_TVECTOR        = 4
local LUA_TSTRING        = 5
local LUA_TTABLE         = 6
local LUA_TFUNCTION      = 7
local LUA_TUSERDATA      = 8
local LUA_TTHREAD        = 9
local LUA_TBUFFER        = 10
local LUA_TPROTO         = 11
local LUA_TUPVAL         = 12
local LUA_TDEADKEY       = 13
local LUA_T_COUNT        = LUA_TPROTO

return function(rt: any, memory_at_0: any, cfns: any, INDIRECT_FUNCTIONS: any, utils: typeof(require("../Common/utils")()))
    return {
        LUA_TNIL           = LUA_TNIL,
        LUA_TBOOLEAN       = LUA_TBOOLEAN,
        LUA_TLIGHTUSERDATA = LUA_TLIGHTUSERDATA,
        LUA_TNUMBER        = LUA_TNUMBER,
        LUA_TVECTOR        = LUA_TVECTOR,
        LUA_TSTRING        = LUA_TSTRING,
        LUA_TTABLE         = LUA_TTABLE,
        LUA_TFUNCTION      = LUA_TFUNCTION,
        LUA_TUSERDATA      = LUA_TUSERDATA,
        LUA_TTHREAD        = LUA_TTHREAD,
        LUA_TBUFFER        = LUA_TBUFFER,
        LUA_TPROTO         = LUA_TPROTO,
        LUA_TUPVAL         = LUA_TUPVAL,
        LUA_TDEADKEY       = LUA_TDEADKEY,
        LUA_T_COUNT        = LUA_T_COUNT,

        LUA_OK        = 0,
        LUA_YIELD     = 1,
        LUA_ERRRUN    = 2,
        LUA_ERRSYNTAX = 3,
        LUA_ERRMEM    = 4,
        LUA_ERRERR    = 5,
        LUA_BREAK     = 6,

        LUA_CORUN = 0,
        LUA_COSUS = 1,
        LUA_CONOR = 2,
        LUA_COFIN = 3,
        LUA_COERR = 4,

        LUA_GCSTOP        = 0,
        LUA_GCRESTART     = 1,
        LUA_GCCOLLECT     = 2,
        LUA_GCCOUNT       = 3,
        LUA_GCCOUNTB      = 4,
        LUA_GCISRUNNING   = 5,
        LUA_GCSTEP        = 6,
        LUA_GCSETGOAL     = 7,
        LUA_GCSETSTEPMUL  = 8,
        LUA_GCSETSTEPSIZE = 9,

        LUA_IDSIZE            = 256,
        LUA_MINSTACK          = 20,
        LUAI_MAXCSTACK        = LUAI_MAXCSTACK,
        LUAI_MAXCALLS         = 20000,
        LUAI_MAXCCALLS        = 200,
        LUA_BUFFERSIZE        = 512,
        LUA_UTAG_LIMIT        = 128,
        LUA_LUTAG_LIMIT       = 128,
        LUA_SIZECLASSES       = 40,
        LUA_MEMORY_CATEGORIES = 256,
        LUA_MINSTRTABSIZE     = 32,
        LUA_MAXCAPTURES       = 32,
        LUA_VECTOR_SIZE       = 3,
        LUA_EXTRA_SIZE        = LUA_VECTOR_SIZE - 2,
        LUA_MULTRET           = -1,
        LUA_REGISTRYINDEX     = LUA_REGISTRYINDEX,
        LUA_ENVIRONINDEX      = LUA_ENVIRONINDEX,
        LUA_GLOBALSINDEX      = LUA_GLOBALSINDEX,
        LUA_TNONE             = -1,
        LUA_NOREF             = -1,
        LUA_REFNIL            = 0,
        LUA_COLIBNAME         = "coroutine",
        LUA_TABLIBNAME        = "table",
        LUA_OSLIBNAME         = "os",
        LUA_STRLIBNAME        = "string",
        LUA_BITLIBNAME        = "bit32",
        LUA_BUFFERLIBNAME     = "buffer",
        LUA_UTF8LIBNAME       = "utf8",
        LUA_MATHLIBNAME       = "math",
        LUA_DBLIBNAME         = "debug",
        LUA_VECLIBNAME        = "vector",

        lua_upvalueindex      = function(i)                      return LUA_GLOBALSINDEX - i end,
        lua_ispseudo          = function(i)                      return if (i <= LUA_REGISTRYINDEX) then 1 else 0 end,
        lua_getref            = function(L, ref)                 return cfns.lua_rawgeti(L, LUA_REGISTRYINDEX, ref) end,
        lua_tonumber          = function(L, i)                   return cfns.lua_tonumberx(L, i, 0) end,
        lua_tointeger         = function(L, i)                   return cfns.lua_tointegerx(L, i, 0) end,
        lua_tounsigned        = function(L, i)                   return cfns.lua_tounsignedx(L, i, 0) end,
        lua_pop               = function(L, n)                   return cfns.lua_settop(L, -n - 1) end,
        lua_newtable          = function(L)                      return cfns.lua_createtable(L, 0, 0) end,
        lua_newuserdata       = function(L, s)                   return cfns.lua_newuserdatatagged(L, s, 0) end,
        lua_strlen            = function(L, i)                   return cfns.lua_objlen(L, i) end,
        lua_isfunction        = function(L, n)                   return if (cfns.lua_type(L, (n)) == LUA_TFUNCTION) then 1 else 0 end,
        lua_istable           = function(L, n)                   return if (cfns.lua_type(L, (n)) == LUA_TTABLE) then 1 else 0 end,
        lua_islightuserdata   = function(L, n)                   return if (cfns.lua_type(L, (n)) == LUA_TLIGHTUSERDATA) then 1 else 0 end,
        lua_isnil             = function(L, n)                   return if (cfns.lua_type(L, (n)) == LUA_TNIL) then 1 else 0 end,
        lua_isboolean         = function(L, n)                   return if (cfns.lua_type(L, (n)) == LUA_TBOOLEAN) then 1 else 0 end,
        lua_isvector          = function(L, n)                   return if (cfns.lua_type(L, (n)) == LUA_TVECTOR) then 1 else 0 end,
        lua_isthread          = function(L, n)                   return if (cfns.lua_type(L, (n)) == LUA_TTHREAD) then 1 else 0 end,
        lua_isbuffer          = function(L, n)                   return if (cfns.lua_type(L, (n)) == LUA_TBUFFER) then 1 else 0 end,
        lua_isnone            = function(L, n)                   return if (cfns.lua_type(L, (n)) == LUA_TNONE) then 1 else 0 end,
        lua_isnoneornil       = function(L, n)                   return if (cfns.lua_type(L, (n)) <= LUA_TNIL) then 1 else 0 end,
        lua_pushliteral       = function(L, s)                   return cfns.lua_pushlstring(L, utils.cstr(""), (#s / 1 --[[sizeof(char)]]) - 1) end,
        lua_pushcfunction     = function(L, fn, debugname)       return cfns.lua_pushcclosurek(L, fn, debugname, 0, 0) end,
        lua_pushcclosure      = function(L, fn, debugname, nup)  return cfns.lua_pushcclosurek(L, fn, debugname, nup, 0) end,
        lua_pushlightuserdata = function(L, p)                   return cfns.lua_pushlightuserdatatagged(L, p, 0) end,
        lua_rawgetp           = function(L, idx, p)              return cfns.lua_rawgetptagged(L, idx, p, 0) end,
        lua_rawsetp           = function(L, idx, p)              return cfns.lua_rawsetptagged(L, idx, p, 0) end,
        lua_setglobal         = function(L, s)                   return cfns.lua_setfield(L, LUA_GLOBALSINDEX, s) end,
        lua_getglobal         = function(L, s)                   return cfns.lua_getfield(L, LUA_GLOBALSINDEX, s) end,
        lua_tostring          = function(L, i)                   return cfns.lua_tolstring(L, i, 0) end,
        lua_pushfstring       = function(L, fmt, ...)            return cfns.lua_pushfstringL(L, fmt, ...) end,
        luaL_error            = function(L, fmt, ...)            return cfns.luaL_errorL(L, fmt, ...) end,
        luaL_typeerror        = function(L, narg, tname)         return cfns.luaL_typeerrorL(L, narg, tname) end,
        luaL_argerror         = function(L, narg, extramsg)      return cfns.luaL_argerrorL(L, narg, extramsg) end,
        luaL_argcheck         = function(L, cond, arg, extramsg) return if cond == 1 then 0 else cfns.luaL_argerror(L, arg, extramsg) end,
        luaL_argexpected      = function(L, cond, arg, tname)    return if cond == 1 then 0 else cfns.luaL_typeerror(L, arg, tname) end,
        luaL_checkstring      = function(L, n)                   return cfns.luaL_checklstring(L, n, 0) end,
        luaL_optstring        = function(L, n, d)                return cfns.luaL_optlstring(L, n, d, 0) end,
        luaL_getmetatable     = function(L, n)                   return cfns.lua_getfield(L, LUA_REGISTRYINDEX, n) end,
        --// luaL_opt         = function(L, f, n, d)             return if (lua_isnoneornil(L, n) == 1) then d else f(L, n) end,
        --// luaL_addchar     = function(B, c)                   return ((void)((B)->p < (B)->end || luaL_prepbuffsize(B, 1)), (*(B)->p++ = (char)(c))) end,
        luaL_addstring        = function(B, s)                   return cfns.luaL_addlstring(B, s, cfns.strlen(s)) end,
    }
end


local common_polyfill_create = require("../Common/polyfill")

return function(utils: typeof(require("../Common/utils")()))
    local rt, memory_at_0, cfns, INDIRECT_FUNCTIONS
    local polyfill, common_polyfill_ready = common_polyfill_create(utils)

    local START_CLOCK, NOW_TIME = os.clock(), os.time() * 1000

    -- returns time since the instance was initialized
    local function now_clock(): number
        return os.clock() - START_CLOCK
    end

    function polyfill.emscripten_date_now()
        return NOW_TIME + now_clock() * 1000
    end

    function polyfill._localtime_js(timer, buf)
        local now = os.time()
        local local_tdata = os.date("*t", rt.i64.into_u64(timer))
        local gmt_offset = os.difftime(now, os.time(os.date("!*t", rt.i64.into_u64(timer))))

        --// https://en.cppreference.com/w/cpp/chrono/c/tm
        rt.store.i32(memory_at_0, buf + 0, local_tdata.sec) --// int tm_sec
        rt.store.i32(memory_at_0, buf + 4, local_tdata.min) --// int tm_min
        rt.store.i32(memory_at_0, buf + 8, local_tdata.hour) --// int tm_hour
        rt.store.i32(memory_at_0, buf + 12, local_tdata.day) --// int tm_mday
        rt.store.i32(memory_at_0, buf + 16, local_tdata.month - 1) --// int tm_mon
        rt.store.i32(memory_at_0, buf + 20, local_tdata.year - 1900) --// int tm_year
        rt.store.i32(memory_at_0, buf + 24, local_tdata.wday - 1) --// int tm_wday
        rt.store.i32(memory_at_0, buf + 28, local_tdata.yday - 1) --// int tm_yday
        rt.store.i32(memory_at_0, buf + 32, if local_tdata.isdst then 1 else 0) --// int tm_isdst
        rt.store.i32(memory_at_0, buf + 36, gmt_offset) --// long tm_gmtoff
    end

    function polyfill._gmtime_js(timer, buf)
        local now = os.time()
        local utc_tdata = os.date("!*t", rt.i64.into_u64(timer))
        local gmt_offset = os.difftime(now, os.time(utc_tdata))

        --// https://en.cppreference.com/w/cpp/chrono/c/tm
        rt.store.i32(memory_at_0, buf + 0, utc_tdata.sec) --// int tm_sec
        rt.store.i32(memory_at_0, buf + 4, utc_tdata.min) --// int tm_min
        rt.store.i32(memory_at_0, buf + 8, utc_tdata.hour) --// int tm_hour
        rt.store.i32(memory_at_0, buf + 12, utc_tdata.day) --// int tm_mday
        rt.store.i32(memory_at_0, buf + 16, utc_tdata.month - 1) --// int tm_mon
        rt.store.i32(memory_at_0, buf + 20, utc_tdata.year - 1900) --// int tm_year
        rt.store.i32(memory_at_0, buf + 24, utc_tdata.wday - 1) --// int tm_wday
        rt.store.i32(memory_at_0, buf + 28, utc_tdata.yday - 1) --// int tm_yday
        rt.store.i32(memory_at_0, buf + 32, if utc_tdata.isdst then 1 else 0) --// int tm_isdst
        rt.store.i32(memory_at_0, buf + 36, gmt_offset) --// long tm_gmtoff
    end

    function polyfill.clock_time_get(clock_id: number, _ignored_precision: boolean, ptime: number)
        if not (clock_id >= 0 and clock_id <= 3) then
            return 28
        end

        local time_now
        if clock_id == 0 then
            time_now = polyfill.emscripten_date_now()
        elseif true then -- if nowIsMonotonic is 1
            time_now = now_clock()
        end

        local nsec = math.round(time_now * 1000 * 1000)
        rt.store.i64(memory_at_0, ptime + 0, rt.i64.from_u64(nsec))
        return 0
    end

    return polyfill, function(_rt: any, _memory_at_0: any, _cfns: any, _INDIRECT_FUNCTIONS: any)
        rt, memory_at_0, cfns, INDIRECT_FUNCTIONS = _rt, _memory_at_0, _cfns, _INDIRECT_FUNCTIONS
        common_polyfill_ready(_rt, _memory_at_0, _cfns, _INDIRECT_FUNCTIONS)
    end
end


local fs = require("@lune/fs")
local process = require("@lune/process")
local regex = require("@lune/regex")

local argparse = require("@self/argparse/src/lib")

local VALID_FLAGS = {
    -- combines runtime and entrypoint into one file
    "BUILD_ONEFILE",
}

local PARSER = argparse("luauceptionbuilder")
PARSER:option("-f --flags", "Flags applied for this building process.", nil, nil, "*"):convert(function(value)
    if not table.find(VALID_FLAGS, value) then
        print(`[WARN]: Flag '{value}' is not defined as valid flag!`)
    end

    return value
end)
-- example: lune run utils/builder -f FLAG1 FLAG2 FLAG3

local LUAU_GIT_HASH, LUAU_GIT_TAG = (function()
    local GIT_EXEC_OPTIONS = {
        cwd = "./luau"
    }

    local git_hash_proc = process.exec(
        "git", {"rev-parse", "HEAD"},
        GIT_EXEC_OPTIONS
    )
    if not git_hash_proc.ok then
        return error("Failed to get git commit hash of luau submodule!")
    end

    local git_tag_proc = process.exec(
        "git", {"describe", "--tags"},
        GIT_EXEC_OPTIONS
    )
    if not git_tag_proc.ok then
        return error("Failed to get git tag name of luau submodule!")
    end

    return string.gsub(git_hash_proc.stdout, "[\r\n]+", ""),
        (string.gsub(git_tag_proc.stdout, "[\r\n]+", "") or "UNKNOWN_TAG")
end)()

type BuilderFlags = {string}
type BuildVariant = "Compiler" | "VM" | "Full"
type SnippetOrder = {any}
type VariantSnippetInstructions = {
    runtime: SnippetOrder,
    entrypoint: SnippetOrder
}

local WASM2LUAU_EXEC: string? = (function()
    local STATIC_PATH = "./utils/wasm2luau"
    if fs.isFile(STATIC_PATH) then
        return STATIC_PATH
    end

    local which_process = process.exec("which", {"wasm2luau"})
    if not which_process.ok then
        return
    end

    return string.gsub(which_process.stdout, "[%s]+", "")
end)()

local HOME_VARIABLE =
    if process.os == "windows" then process.env["USERPROFILE"]
    else process.env["HOME"]

local WASM_OUTPUT_DIR = "./build"
local BUILD_DIR = "./output"

local VARIANT_PREFIX = "Luau.LuauCeption"

local function getRokitExec(exec: string): string
	return `{HOME_VARIABLE}/.rokit/bin/{exec}`
end

local function createHeaderSnippet(variant: BuildVariant): string
    local result = {
        `--// LuauCeption - {variant} Variant`,
        `--// Built from Luau {LUAU_GIT_TAG} ({LUAU_GIT_HASH})\n\n`,
    }
    return table.concat(result, "\n")
end

local CONTENT_VARIABLE = newproxy(false)

local FLAGGED_PATCHES = {
    ["BUILD_ONEFILE"] = {
        ["IMPORT_PATTERN"] = "\\(require\\)\\(['\"]@self\\/runtime['\"]\\)",
        ["IMPORT_REPLACEMENT"] = "__LUAUCEPTION_RUNTIME__",
        ["RUNTIME_REPLACEMENT"] = "&RUNTIME&",
        ["ENCAPSULATION_CODE"] = "local __LUAUCEPTION_RUNTIME__ = (function()\n\n&RUNTIME&\nend)()\n",
    },
}

local FLAGGED_PATCHES_SNIPPET_ORDER = {
    ["BUILD_ONEFILE"] = {
        {"gotoline", 5},
        {"putstring", "\n\n"},
        {"putstring", FLAGGED_PATCHES.BUILD_ONEFILE.ENCAPSULATION_CODE},
        {"replaceall", FLAGGED_PATCHES.BUILD_ONEFILE.IMPORT_PATTERN, FLAGGED_PATCHES.BUILD_ONEFILE.IMPORT_REPLACEMENT},
        -- we can't use the replaceall instruction here because
        -- file would be modified by the regex, removing regex-unsafe strings
        -- which makes the runtime have unexpected errors
        {"goto", "end", FLAGGED_PATCHES.BUILD_ONEFILE.RUNTIME_REPLACEMENT},
        {"removelinecursor"},
        {"putstring", CONTENT_VARIABLE, "LUAUCEPTION_RUNTIME"},
    }
}

local VARIANT_BUILD_SNIPPET_ORDER: {[string]: VariantSnippetInstructions} = {
    ["Compiler"] = {
        ["runtime"] = {
            {"jmpifflag", 2, "BUILD_ONEFILE", "defined"},
            {"goto", "end", "--!optimize 2\n"},
            {"putstring", createHeaderSnippet("Compiler")},
            {"jmpifflag", 2, "BUILD_ONEFILE", "undefined"},
            {"gotopos", 1},
            {"removelinecursor"},
            {"putfile", "Common/vector3_compat.luau"},
        },
        ["entrypoint"] = {
            {"putfile", "Compiler/entrypoint.luau"},
            {"process", "./src/builder/build.darklua.json"},

            {"gotopos", 1},
            {"putstring", "--!optimize 2\n"},
            {"putstring", createHeaderSnippet("Compiler")},
            {"putstring", "local STUB_WARN = false\n"},
        },
    },
    ["VM"] = {
        ["runtime"] = {
            {"jmpifflag", 2, "BUILD_ONEFILE", "defined"},
            {"goto", "end", "--!optimize 2\n"},
            {"putstring", createHeaderSnippet("Compiler")},
            {"jmpifflag", 2, "BUILD_ONEFILE", "undefined"},
            {"gotopos", 1},
            {"removelinecursor"},
            {"putfile", "Common/vector3_compat.luau"},
        },
        ["entrypoint"] = {
            {"putfile", "VM/entrypoint.luau"},
            {"process", "./src/builder/build.darklua.json"},

            {"gotopos", 1},
            {"putstring", "--!optimize 2\n"},
            {"putstring", createHeaderSnippet("VM")},
            {"putstring", "local STUB_WARN = false\n"},
        },
    },
    ["Full"] = {
        ["runtime"] = {
            {"jmpifflag", 2, "BUILD_ONEFILE", "defined"},
            {"goto", "end", "--!optimize 2\n"},
            {"putstring", createHeaderSnippet("Compiler")},
            {"jmpifflag", 2, "BUILD_ONEFILE", "undefined"},
            {"gotopos", 1},
            {"removelinecursor"},
            {"putfile", "Common/vector3_compat.luau"},
        },
        ["entrypoint"] = {
            {"putfile", "Full/entrypoint.luau"},
            {"process", "./src/builder/build.darklua.json"},

            {"gotopos", 1},
            {"putstring", "--!optimize 2\n"},
            {"putstring", createHeaderSnippet("Full")},
            {"putstring", "local STUB_WARN = false\n"},
        },
    },
}

local function getAbsolutePath(relative_path: string): string
    return `{process.cwd}/{relative_path}`
end

local function processSnippetOrder(
    snippet_orders: {SnippetOrder},
    flags: BuilderFlags,
    variables: {string}?,
    input: string
): (string, number)
    local _VARS: {string} = variables or {}
    local exit_code = 0
    local pc, contentpos = 0, 1
    local result = input

    while true do
        if pc >= #snippet_orders then
            break
        end

        pc += 1
        local order = table.clone(snippet_orders[pc])
        local command = table.remove(order, 1)

        if command == "goto" then
            local pattern = regex.new(order[2]):find(result)
            if not pattern then
                print(`Cannot find pattern '{order[2]}' on goto command`)
                continue
            end

            local position_mode = order[1]
            if position_mode == "start" then
                contentpos = pattern.start - 1
            elseif position_mode == "end" then
                contentpos = pattern.finish + 1
            end
        elseif command == "runsnippet" then
            local snippets = table.clone(order[1])
            table.move(snippets, 1, #snippets, #snippet_orders, snippet_orders)
        elseif command == "jmpifflag" then
            local pos_offset, flag_name, action = unpack(order)
            if not table.find(VALID_FLAGS, flag_name) then
                print(`[WARN]: Flag '{flag_name}' is not valid flag, but is included in snippet ordering!`)
                continue
            end

            local flag_defined = not not table.find(flags, flag_name)

            if action == "defined" then
                pc += if (flag_defined) then pos_offset else 0
            elseif action == "undefined" then
                pc += if (not flag_defined) then pos_offset else 0
            end
        elseif command == "gotopos" then
            contentpos = order[1]
        elseif command == "gotoline" then
            local newline_pos = 0

            for _ = 1, order[1] do
                local start_pos, end_pos = string.find(result, "[^\n]*\n", newline_pos + 1)
                if not start_pos then
                    error("Provided line count exceeded!")
                end

                newline_pos = end_pos
            end
            contentpos = newline_pos
        elseif command == "removelinecursor" then
            local str_start = string.sub(result, 1, contentpos - 1)
            local str_end = string.sub(result, contentpos)
            local pre = string.match(str_start, "^(.*)\n[^\n]*$") or ""
            local post = string.match(str_end, "^[^\n]*\n?(.*)$") or ""
            local corrected_pos = string.match(str_start, ".*()\n")

            result = pre .. post
            contentpos = corrected_pos or contentpos
        elseif command == "removelines" then
            local current_str = string.sub(result, contentpos)
            local newline_pos = 0

            for _ = 1, order[1] do
                local start_pos, end_pos = string.find(current_str, "[^\n]*\n", newline_pos + 1)
                if not start_pos then
                    error("Provided line count exceeded!")
                end

                newline_pos = end_pos
            end
            result = string.sub(result, 1, contentpos - 1) .. string.sub(result, contentpos + newline_pos)
        elseif command == "putstring" or command == "putfile" then
            local content: string
            if command == "putfile" then
                local file_path = getAbsolutePath(`./snippets/{order[1]}`)
                if not fs.isFile(file_path) then
                    return error(`Cannot find assigned file '{order[1]}' on putfile command!`)
                end

                content = fs.readFile(file_path) .. "\n"
            else
                content = if order[1] == CONTENT_VARIABLE then _VARS[order[2]] else order[1]
            end

            local content_len = (utf8.len(content) :: number) + 1
            local str_start = string.sub(result, 0, contentpos - 1)
            local str_end = string.sub(result, contentpos)

            result = str_start .. content .. str_end
            contentpos = #str_start + content_len
        elseif command == "replaceall" then
            local pattern = regex.new(order[1])
            result = pattern:replaceAll(result, if order[2] == CONTENT_VARIABLE then _VARS[order[3]] else order[2])
        elseif command == "process" then
            local temp_dir_path = getAbsolutePath("./snippets/_temp")
            if not fs.isDir(temp_dir_path) then
                fs.writeDir(temp_dir_path)
            end

            local darklua_file_path = getRokitExec("darklua")
            if not fs.isFile(darklua_file_path) then
                return error("Cannot find darklua executable file which is needed on process command!")
            end

            local config_file = getAbsolutePath(order[1])
            if not fs.isFile(config_file) then
                return error(`Cannot find provided darklua config file '{order[1]}' on process command!`)
            end

            local process_id = os.time()
            local temp_file_path = `{temp_dir_path}/__{process_id}.luau`
            local temp_file_result_path = getAbsolutePath(`./utils/_tempresult{process_id}.luau`)

            fs.writeFile(temp_file_path, result)

            local exec_result = process.exec(
                darklua_file_path,
                {
                    "process",
                    temp_file_path,
                    temp_file_result_path,
                    "-c",
                    config_file
                }
            )
            if exec_result.ok then
                result = fs.readFile(temp_file_result_path)
                fs.removeFile(temp_file_path)
                fs.removeFile(temp_file_result_path)
            else
                fs.readFile(temp_file_path)
                return error(`Process command failed, log:\n{exec_result.stderr}`, 0)
            end
        else
            error(`Unknown instruction '{command}' provided!`)
        end
    end
    return result, exit_code
end

local function createBuildVariant(variant: BuildVariant, flags: BuilderFlags)
    local snippet_orders = VARIANT_BUILD_SNIPPET_ORDER[variant]
    if not snippet_orders then
        return error(`Invalid variant type '{variant}'`)
    end

    local variant_id = `{VARIANT_PREFIX}.{variant}`
    local wasm_file = getAbsolutePath(`{WASM_OUTPUT_DIR}/{variant_id}.wasm`)
    local variant_path = getAbsolutePath(`{BUILD_DIR}/{variant_id}`)

    if not fs.isFile(wasm_file) then
        return error(`Variant '{variant}' WASM output not found!`)
    end

    local wasynth_proc = process.create(WASM2LUAU_EXEC :: string, {wasm_file})
    local wasynth_output = wasynth_proc.stdout:readToEnd()
    if not wasynth_proc:status().ok then
        return error(`Failed to transpile WASM output of variant '{variant}'`)
    end
    print(`Variant '{variant}' got transpiled to luau successfully.`)

    local processed_runtime = processSnippetOrder(snippet_orders.runtime, flags, nil, wasynth_output)
    if not processed_runtime then
        return error(`Failed to process runtime of variant '{variant}'`)
    end
    print(`Successfully processed transpiled runtime of variant '{variant}'!`)

    local processed_entrypoint = processSnippetOrder(snippet_orders.entrypoint, flags, nil, "")
    if not processed_entrypoint then
        return error(`Failed to process variant '{variant}'`)
    end
    print(`Successfully processed entrypoint of variant '{variant}'!`)

    if not table.find(flags, "BUILD_ONEFILE") then
        local __VARIANT_FILE = `{variant_path}.luau`
        if fs.isFile(__VARIANT_FILE) then
            fs.removeFile(__VARIANT_FILE)
        end

        if not fs.isDir(variant_path) then
            fs.writeDir(variant_path)
        end

        fs.writeFile(`{variant_path}/runtime.luau`, processed_runtime)
        fs.writeFile(`{variant_path}/init.luau`, processed_entrypoint)
        return
    end

    local onefile_result = processSnippetOrder(
        FLAGGED_PATCHES_SNIPPET_ORDER.BUILD_ONEFILE,
        flags,
        {
            LUAUCEPTION_RUNTIME = processed_runtime
        },
        processed_entrypoint
    )
    if fs.isDir(variant_path) then
        fs.removeDir(variant_path)
    end

    fs.writeFile(`{variant_path}.luau`, onefile_result)
    return
end

do
    if not WASM2LUAU_EXEC then
        print("Please install wasm2luau by running ./utils/setup_wasm2luau.sh first!")
        return
    end

    local args = PARSER:parse()
    if not fs.isDir(getAbsolutePath(BUILD_DIR)) then
        fs.writeDir(getAbsolutePath(BUILD_DIR))
    end

    for _, variant in {"Compiler", "VM", "Full"} do
        createBuildVariant(variant, args.flags or {})
    end
end


local frktest = require("@frktest/frktest")
local console_reporter = require("@frktest/reporters/console_reporter")
local process = require("@lune/process")

require("./suites/compiler")
require("./suites/vm")
require("./suites/full")

console_reporter.init()
local testOk = frktest.run()
if (not testOk) then
    process.exit(1)
end


local frktest = require("@frktest/frktest")
local test = frktest.test
local check = frktest.assert.check
local req = frktest.assert.require
local msg = frktest.assert.msg

test.suite("Compiler", function()
    local Compiler = require("../../../output/Luau.LuauCeption.Compiler")
    for opt = 0, 2 do
        for dbg = 0, 2 do
            for typInf = 0, 2 do
                for cov = 0, 2 do
                    test.case(`should be able to compile valid Luau code without erroring with opt {opt} dbg {dbg} typInf {typInf} cov {cov}`, function()
                        check.should_not_error(function()
                            Compiler.luau_compile(`print("Hello world!")\nprint({opt},{dbg},{typInf},{cov})`, {
                                OptimizationLevel = opt,
                                DebugLevel = dbg,
                                TypeInfoLevel = typInf,
                                CoverageLevel = cov
                            })
                            return
                        end)
                    end)

                    test.case(`shouldn't be able to compile invalid Luau code without erroring with opt {opt} dbg {dbg} typInf {typInf} cov {cov}`, function()
                        check.should_not_error(function()
                            Compiler.luau_compile(`whar`, {
                                OptimizationLevel = opt,
                                DebugLevel = dbg,
                                TypeInfoLevel = typInf,
                                CoverageLevel = cov
                            })
                            return
                        end)
                    end)

                    test.case("first byte for invalid code from compiler should be null (0) with opt {opt} dbg {dbg} typInf {typInf} cov {cov}", function()
                        local bc, bcs = Compiler.luau_compile(`whar`, {
                            OptimizationLevel = opt,
                            DebugLevel = dbg,
                            TypeInfoLevel = typInf,
                            CoverageLevel = cov
                        })
                        msg(`bc: "{bc}", bcs: {bcs}, first byte: {string.byte(string.sub(bc, 1, 1))}`)
                        check.equal(string.byte(string.sub(bc, 1, 1)), 0)
                    end)
                end
            end
        end
    end
end)

return true


local frktest = require("@frktest/frktest")
local test = frktest.test
local check = frktest.assert.check
local req = frktest.assert.require
local msg = frktest.assert.msg

test.suite("Full", function()
    local Full = require("../../../output/Luau.LuauCeption.Full")
    for opt = 0, 2 do
        for dbg = 0, 2 do
            for typInf = 0, 2 do
                for cov = 0, 2 do
                    test.case(`should be able to compile valid Luau code without erroring with opt {opt} dbg {dbg} typInf {typInf} cov {cov}`, function()
                        check.should_not_error(function()
                            Full.luau_compile(`print("Hello world!")\nprint({opt},{dbg},{typInf},{cov})`, {
                                OptimizationLevel = opt,
                                DebugLevel = dbg,
                                TypeInfoLevel = typInf,
                                CoverageLevel = cov
                            })
                            return
                        end)
                    end)

                    test.case(`shouldn't be able to compile invalid Luau code without erroring with opt {opt} dbg {dbg} typInf {typInf} cov {cov}`, function()
                        check.should_not_error(function()
                            Full.luau_compile(`whar`, {
                                OptimizationLevel = opt,
                                DebugLevel = dbg,
                                TypeInfoLevel = typInf,
                                CoverageLevel = cov
                            })
                            return
                        end)
                    end)

                    test.case("first byte for invalid code from compiler should be null (0) with opt {opt} dbg {dbg} typInf {typInf} cov {cov}", function()
                        local bc, bcs = Full.luau_compile(`whar`, {
                            OptimizationLevel = opt,
                            DebugLevel = dbg,
                            TypeInfoLevel = typInf,
                            CoverageLevel = cov
                        })
                        msg(`bc: "{bc}", bcs: {bcs}, first byte: {string.byte(string.sub(bc, 1, 1))}`)
                        check.equal(string.byte(string.sub(bc, 1, 1)), 0)
                    end)

                    test.case(`should be able to run valid Luau code without erroring with opt {opt} dbg {dbg} typInf {typInf} cov {cov}`, function()
                        local bc, bcs = Full.luau_compile(`print("Hello world!")\nprint({opt},{dbg},{typInf},{cov})`, {
                            OptimizationLevel = opt,
                            DebugLevel = dbg,
                            TypeInfoLevel = typInf,
                            CoverageLevel = cov
                        })

                        msg(`no safe env, bcs: {bcs}`)
                        check.should_not_error(function()
                            Full.luau_run(bc, `LuauCeptionTest_VM_NSF_{opt},{dbg},{typInf},{cov}`, false)
                            return
                        end)

                        msg(`safe env, bcs: {bcs}`)
                        check.should_not_error(function()
                            Full.luau_run(bc, `LuauCeptionTest_VM_WSF_{opt},{dbg},{typInf},{cov}`, true)
                            return
                        end)
                    end)
                end
            end
        end
    end

    test.case("should be able to handle longjmp", function()
        local bc = Full.luau_compile([[
            local function main()
                error("longjmp test kaboom")
            end

            print(pcall(main))
        ]], {
            OptimizationLevel = 1,
            DebugLevel = 1,
            TypeInfoLevel = 1,
            CoverageLevel = 0
        })

        check.should_not_error(function()
            Full.luau_run(bc, "LuauCeptionTest_LongJMP_Handling_NSF", false)
            return
        end)

        check.should_not_error(function()
            Full.luau_run(bc, "LuauCeptionTest_LongJMP_Handling_WSF", true)
            return
        end)
    end)
end)

return true


local frktest = require("@frktest/frktest")
local test = frktest.test
local check = frktest.assert.check
local req = frktest.assert.require
local msg = frktest.assert.msg

test.suite("VM", function()
    local Compiler = require("../../../output/Luau.LuauCeption.Compiler")
    local VM = require("../../../output/Luau.LuauCeption.VM")
    for opt = 0, 2 do
        for dbg = 0, 2 do
            for typInf = 0, 2 do
                for cov = 0, 2 do
                    test.case(`should be able to run valid Luau code without erroring with opt {opt} dbg {dbg} typInf {typInf} cov {cov}`, function()
                        local bc, bcs = Compiler.luau_compile(`print("Hello world!")\nprint({opt},{dbg},{typInf},{cov})`, {
                            OptimizationLevel = opt,
                            DebugLevel = dbg,
                            TypeInfoLevel = typInf,
                            CoverageLevel = cov
                        })

                        msg(`no safe env, bcs: {bcs}`)
                        check.should_not_error(function()
                            VM.luau_run(bc, `LuauCeptionTest_VM_NSF_{opt},{dbg},{typInf},{cov}`, false)
                            return
                        end)

                        msg(`safe env, bcs: {bcs}`)
                        check.should_not_error(function()
                            VM.luau_run(bc, `LuauCeptionTest_VM_WSF_{opt},{dbg},{typInf},{cov}`, true)
                            return
                        end)
                    end)
                end
            end
        end
    end

    test.case("should be able to handle longjmp", function()
        local bc = Compiler.luau_compile([[
            local function main()
                error("longjmp test kaboom")
            end

            print(pcall(main))
        ]], {
            OptimizationLevel = 1,
            DebugLevel = 1,
            TypeInfoLevel = 1,
            CoverageLevel = 0
        })

        check.should_not_error(function()
            VM.luau_run(bc, "LuauCeptionTest_LongJMP_Handling_NSF", false)
            return
        end)

        check.should_not_error(function()
            VM.luau_run(bc, "LuauCeptionTest_LongJMP_Handling_WSF", true)
            return
        end)
    end)
end)

return true


local src = [[
for i = 1, 10, 1 do
    print(i)
end
]]

local Compiler = require("../../output/Luau.LuauCeption.Compiler")
local VM = require("../../output/Luau.LuauCeption.VM")
local Full = require("../../output/Luau.LuauCeption.Full")

print("Compiler")
print(Compiler.luau_compile(src, 1, 1, 0))
print(Compiler.luau_compile(src, 1, 2, 0))
print(Compiler.luau_compile(src, 1, 1, 1))
print(Compiler.luau_compile(src, 1, 2, 1))

print("VM")
local bc = Compiler.luau_compile(src, 1, 2, 1)
print(VM.luau_run(bc, "test", true))
print(VM.luau_run(bc, "test", false))

print("Full")
local bc = Full.luau_compile(src, 1, 2, 1)
print(Full.luau_compile(src, 1, 1, 0))
print(Full.luau_compile(src, 1, 2, 0))
print(Full.luau_compile(src, 1, 1, 1))
print(Full.luau_compile(src, 1, 2, 1))
print(Full.luau_run(bc, "test", true))
print(Full.luau_run(bc, "test", false))

print("\nTest ok")


local luau = require("Luau.LuauCeption.Full.0.645")
local ift = luau.wasm.indirect_function_table.data
local cfns = luau.wasm.cfns
local rt = luau.wasm.rt
local memory_at_0 = luau.wasm.memory
local macros = luau.wasm.macros

local function cstr(str)
    local strLen = #str
    local strPtr = cfns.malloc(strLen + 1)
    rt.store.string(memory_at_0, strPtr, str)
    rt.store.i32_n8(memory_at_0, strPtr + strLen, 0)
    return strPtr
end

local function insertCFunction(f: () -> ())
    local ptr = #ift + 1
    ift[ptr] = f
    return ptr
end

local testCFn = insertCFunction(function(L)
    print("lua_State: ", L)
    cfns.lua_pushstring(L, cstr("real"))
    return 1
end)

local L = cfns.luaL_newstate()
cfns.luaL_openlibs(L)
cfns.lua_pushcclosurek(L, testCFn, cstr("luauception.test"), 0, 0)
macros.lua_setglobal(L, cstr("lctest"))

local bc, bcs = luau.luau_compile([[print(lctest(), vector)]])

local bcPtr = cstr(bc)
cfns.luau_load(L, cstr("luauception.chunk"), bcPtr, #bc, 0)
local clRes = cfns.lua_pcall(L, 0, 0, 0)

if (clRes == macros.LUA_OK) then
    print("lua ok")
elseif (clRes == macros.LUA_ERRRUN) then
    local e = macros.lua_tostring(L, -1)
    print(rt.load.string(memory_at_0, e, cfns.strlen(e)))
elseif (clRes == macros.LUA_ERRMEM) then
    print("lua out of memory")
elseif (clRes == macros.LUA_ERRERR) then
    local e = macros.lua_tostring(L, -1)
    print("error func errored ", rt.load.string(memory_at_0, e, cfns.strlen(e)))
end

cfns.lua_close(L)


local CHECKBOX_EMOJI = utf8.char(0x2705)
local CROSS_EMOJI = utf8.char(0x274C)

local function getSuccess(fn, ...)
	return ( pcall(fn, ...) )
end

local function customSuccess(fn)
	local success, result = pcall(fn)
	return success and result
end

local capInfos = {
	{
		name = "Plugin",
		index = 0,
		canAccess = getSuccess(function()
			return game:GetService("CoreGui").Name
		end)
	},
	{
		name = "LocalUser",
		index = 1,
		canAccess = getSuccess(function()
			return game.DataCost
		end)
	},
	{
		name = "WritePlayer",
		index = 2,
		canAccess = getSuccess(Instance.new, "Player")
	},
	{
		name = "RobloxScript",
		index = 3,
		canAccess = getSuccess(function()
			return game:GetService("CorePackages").Name
		end)
	},
	{
		name = "RobloxEngine",
		index = 4,
		canAccess = getSuccess(function()
			return Instance.new("Decal").TexturePack
		end)
	},
	{
		name = "NotAccessible",
		index = 5,
		canAccess = getSuccess(function()
			Instance.new("MeshPart").MeshId = ""
		end)
	},

	{
		name = "RemoteCommand",
		index = 59,
		canAccess = customSuccess(function()
			local RemoteCommandService = game:GetService("RemoteCommandService")

			if not RemoteCommandService then
				return false
			end

			local success, errMsg = pcall(RemoteCommandService.GetExecutingPlayer, RemoteCommandService)

			if not success and string.find(errMsg, "current thread cannot") then
				return false
			end

			return true
		end)
	},
	{
		name = "InternalTest",
		index = 60,
		canAccess = getSuccess(Instance.new, "CustomLog")
	},
	{
		name = "PluginOrOpenCloud",
		index = 61,
		canAccess = getSuccess(function()
			Instance.new("ModuleScript").Source = ""
		end)
	},
	-- nothing in the engine has Assistant anymore as StreamingService was deleted, not worth to check against anymore
	--[[{
		name = "Assistant",
		index = 62,
		canAccess = customSuccess(function()
			local StreamingService = game:GetService("StreamingService")

			if not StreamingService then
				return false
			end

			-- this is going to error regardless because noargs, lacking permission takes priority though
			local _, errMsg = pcall(StreamingService.ExecuteCommandAsync, StreamingService)

			if string.find(errMsg, "current thread cannot") then
				return false
			end

			return true
		end)
	},]]
}

local longestNameLength = 0
for _, info in capInfos do
	if #info.name > longestNameLength then
		longestNameLength = #info.name
	end
end

local message = string.format(`\n%-{longestNameLength}s | %-6s | %s\n`, "Name", "Index", "Accessible")
message ..= string.rep("-", #message)

for _, info in capInfos do
	local index = if info.index then tostring(info.index) else "N/A"
	local accessible = info.canAccess and CHECKBOX_EMOJI or CROSS_EMOJI

	message ..= string.format(`\n%-{longestNameLength}s | %-6s | %s`, info.name, index, accessible)
end

printidentity("Approximate Capabilities for Identity")
print(message)


local Selection = game:GetService("Selection")
local StarterGui = game:GetService("StarterGui")

local Root = script:FindFirstAncestor("Metain")
local Utils = require(Root.Utils)

local InstanceCmds = {}

function InstanceCmds.create_instance(data: {[string]: any}): {[string]: any}
	local className = data.className
	if not className then
		return {success = false, message = "Missing className"}
	end

	local parent = data.parent and (Utils.findByPath(data.parent) or Utils.locationMap[data.parent]) or nil
	if not parent then
		return {success = false, message = "Invalid parent: " .. tostring(data.parent)}
	end

	local ok, instance = pcall(function()
		return Instance.new(className)
	end)
	if not ok then
		return {success = false, message = "Invalid className: " .. className}
	end

	Utils.withRecording("Create Instance", function()
		instance.Name = data.name or className
		if data.properties then
			for propName, propValue in data.properties do
				pcall(function()
					(instance :: any)[propName] = Utils.convertValue(propValue)
				end)
			end
		end
		instance.Parent = parent
	end)

	Selection:Set({instance})
	return {success = true, message = "Created: " .. instance:GetFullName()}
end

function InstanceCmds.create_ui(data: {[string]: any}): {[string]: any}
	local ui = Utils.createFromSpec(data.spec)

	local parent = StarterGui
	if data.parent then
		local resolved, resolveErr = Utils.requireTarget(data.parent)
		if resolveErr then return resolveErr end
		parent = resolved :: Instance
	end

	Utils.withRecording("Create UI", function()
		ui.Parent = parent
	end)

	Selection:Set({ui})
	return {success = true, message = "Created: " .. ui:GetFullName()}
end

function InstanceCmds.rename(data: {[string]: any}): {[string]: any}
	local target, err = Utils.requireTarget(data.path)
	if err then return err end

	local oldName = (target :: Instance).Name
	Utils.withRecording("Rename", function()
		(target :: Instance).Name = data.newName
	end)

	Selection:Set({target :: Instance})
	return {success = true, message = "Renamed: " .. oldName .. " -> " .. data.newName}
end

function InstanceCmds.move(data: {[string]: any}): {[string]: any}
	local target, err = Utils.requireTarget(data.path)
	if err then return err end

	local newParent = Utils.findByPath(data.newParent) or Utils.locationMap[data.newParent]
	if not newParent then
		return {success = false, message = "Invalid destination: " .. tostring(data.newParent)}
	end

	Utils.withRecording("Move", function()
		(target :: Instance).Parent = newParent
	end)

	Selection:Set({target :: Instance})
	return {success = true, message = "Moved to: " .. (target :: Instance):GetFullName()}
end

function InstanceCmds.delete(data: {[string]: any}): {[string]: any}
	local target = Utils.findByPath(data.path) or (Utils.locationMap[data.location] and Utils.locationMap[data.location]:FindFirstChild(data.name))
	if not target then
		return {success = false, message = "Not found"}
	end

	local fullName = target:GetFullName()
	Utils.withRecording("Delete", function()
		target.Parent = nil
	end)

	return {success = true, message = "Deleted: " .. fullName}
end

function InstanceCmds.clone(data: {[string]: any}): {[string]: any}
	local target, err = Utils.requireTarget(data.path)
	if err then return err end

	local cloned = (target :: Instance):Clone()
	if not cloned then
		return {success = false, message = "Cannot clone (Archivable is false)"}
	end

	cloned.Name = data.newName or ((target :: Instance).Name .. "_Clone")
	local newParent = data.newParent and (Utils.findByPath(data.newParent) or Utils.locationMap[data.newParent]) or (target :: Instance).Parent
	Utils.withRecording("Clone", function()
		cloned.Parent = newParent
	end)

	Selection:Set({cloned})
	return {success = true, message = "Cloned: " .. cloned:GetFullName()}
end

function InstanceCmds.insert_asset(data: {[string]: any}): {[string]: any}
	local assetId = tonumber(data.assetId)
	if not assetId then
		return {success = false, message = "Missing or invalid assetId"}
	end

	local parent = workspace
	if data.parent then
		parent = Utils.findByPath(data.parent) or Utils.locationMap[data.parent]
		if not parent then
			return {success = false, message = "Invalid parent: " .. tostring(data.parent)}
		end
	end

	local insertedInstance
	Utils.withRecording("Insert Asset", function()
		local ok, container = pcall(function()
			return game:GetService("InsertService"):LoadAsset(assetId)
		end)
		if not ok then
			error("Could not load asset: " .. tostring(container))
		end

		local children = container:GetChildren()
		if #children == 0 then
			container:Destroy()
			error("Asset is empty")
		end

		if #children == 1 then
			children[1].Parent = parent
			insertedInstance = children[1]
			container:Destroy()
		else
			container.Parent = parent
			insertedInstance = container
		end
	end)

	Selection:Set({insertedInstance})
	return {success = true, message = "Inserted: " .. insertedInstance:GetFullName(), data = {name = insertedInstance.Name}}
end

return InstanceCmds


local ChangeHistoryService = game:GetService("ChangeHistoryService")

local Root = script:FindFirstAncestor("Metain")
local Utils = require(Root.Utils)

local PropertyCmds = {}

function PropertyCmds.set_property(data: {[string]: any}): {[string]: any}
	local target, targetErr = Utils.requireTarget(data.path)
	if targetErr then return targetErr end

	local recording = ChangeHistoryService:TryBeginRecording("Metain: Set Property")
	local ok, err = pcall(function()
		((target :: Instance) :: any)[data.property] = Utils.convertValue(data.value)
	end)
	if recording then
		ChangeHistoryService:FinishRecording(recording, ok and Enum.FinishRecordingOperation.Commit or Enum.FinishRecordingOperation.Cancel)
	end

	if ok then
		return {success = true, message = "Set " .. data.property .. " on " .. (target :: Instance).Name}
	else
		return {success = false, message = "Failed: " .. tostring(err)}
	end
end

function PropertyCmds.set_properties(data: {[string]: any}): {[string]: any}
	local target, targetErr = Utils.requireTarget(data.path)
	if targetErr then return targetErr end

	local properties = data.properties
	if not properties or typeof(properties) ~= "table" or not next(properties) then
		return {success = false, message = "Missing or empty properties table"}
	end

	local recording = ChangeHistoryService:TryBeginRecording("Metain: Set Properties")
	local set = {}
	local failed = {}

	for propName, propValue in properties do
		local ok, err = pcall(function()
			((target :: Instance) :: any)[propName] = Utils.convertValue(propValue)
		end)
		if ok then
			table.insert(set, propName)
		else
			table.insert(failed, propName .. ": " .. tostring(err))
		end
	end

	if recording then
		local operation = #set > 0 and Enum.FinishRecordingOperation.Commit or Enum.FinishRecordingOperation.Cancel
		ChangeHistoryService:FinishRecording(recording, operation)
	end

	if #failed > 0 then
		return {success = #set > 0, message = "Set " .. #set .. ", failed " .. #failed .. ": " .. table.concat(failed, "; ")}
	end
	return {success = true, message = "Set " .. table.concat(set, ", ") .. " on " .. (target :: Instance).Name}
end

function PropertyCmds.add_tag(data: {[string]: any}): {[string]: any}
	local target, err = Utils.requireTarget(data.path)
	if err then return err end

	Utils.withRecording("Add Tag", function()
		(target :: Instance):AddTag(data.tag)
	end)

	return {success = true, message = "Added tag '" .. data.tag .. "' to " .. (target :: Instance).Name}
end

function PropertyCmds.remove_tag(data: {[string]: any}): {[string]: any}
	local target, err = Utils.requireTarget(data.path)
	if err then return err end

	Utils.withRecording("Remove Tag", function()
		(target :: Instance):RemoveTag(data.tag)
	end)

	return {success = true, message = "Removed tag '" .. data.tag .. "' from " .. (target :: Instance).Name}
end

function PropertyCmds.set_attribute(data: {[string]: any}): {[string]: any}
	local target, err = Utils.requireTarget(data.path)
	if err then return err end

	Utils.withRecording("Set Attribute", function()
		(target :: Instance):SetAttribute(data.attribute, Utils.convertValue(data.value))
	end)

	return {success = true, message = "Set attribute '" .. data.attribute .. "' on " .. (target :: Instance).Name}
end

function PropertyCmds.undo(data: {[string]: any}): {[string]: any}
	local count = data.count or 1
	local undone = 0
	for i = 1, count do
		local canUndo, waypoint = ChangeHistoryService:GetCanUndo()
		if canUndo and waypoint and tostring(waypoint):find("^Metain: ") then
			ChangeHistoryService:Undo()
			undone = undone + 1
		else
			break
		end
	end
	if undone == 0 then
		return {success = false, message = "Nothing to undo"}
	end
	return {success = true, message = "Undid " .. undone .. " action(s)", data = {undone = undone}}
end

return PropertyCmds


local ScriptEditorService = game:GetService("ScriptEditorService")
local Selection = game:GetService("Selection")

local Root = script:FindFirstAncestor("Metain")
local Utils = require(Root.Utils)

local ScriptCmds = {}

function ScriptCmds.read_script(data: {[string]: any}): {[string]: any}
	local target, err = Utils.requireScript(data.path)
	if err then return err end

	local source = ""
	pcall(function()
		source = ScriptEditorService:GetEditorSource(target :: Instance)
	end)

	return {
		success = true,
		source = source,
		name = (target :: Instance).Name,
		className = (target :: Instance).ClassName,
		path = (target :: Instance):GetFullName(),
		message = "Read: " .. (target :: Instance):GetFullName()
	}
end

function ScriptCmds.create_script(data: {[string]: any}): {[string]: any}
	local scriptType = data.scriptType or "Script"

	local parent
	if data.parent then
		parent = Utils.findByPath(data.parent) or Utils.locationMap[data.parent]
	else
		parent = Utils.locationMap[data.location]
	end

	if not parent then
		return {success = false, message = "Invalid location/parent: " .. tostring(data.parent or data.location)}
	end

	if not data.source then
		return {success = false, message = "Missing source code - cannot create script"}
	end

	local newScript = Instance.new(scriptType)
	newScript.Name = data.name or "NewScript";
	(newScript :: any).Source = data.source
	Utils.withRecording("Create Script", function()
		newScript.Parent = parent
	end)

	Selection:Set({newScript})
	return {success = true, message = "Created: " .. newScript:GetFullName()}
end

function ScriptCmds.update_script(data: {[string]: any}): {[string]: any}
	local target = Utils.findByPath(data.path) or (Utils.locationMap[data.location] and Utils.locationMap[data.location]:FindFirstChild(data.name))
	if not target or not target:IsA("LuaSourceContainer") then
		return {success = false, message = "Script not found"}
	end

	if not data.source or data.source == "" then
		return {success = false, message = "Missing source code - cannot update script"}
	end

	local parent = target.Parent
	local replacement = target:Clone();
	(replacement :: any).Source = data.source
	Utils.withRecording("Update Script", function()
		replacement.Parent = parent
		target.Parent = nil
	end)

	Selection:Set({replacement})
	return {success = true, message = "Updated: " .. replacement:GetFullName()}
end

function ScriptCmds.edit_script(data: {[string]: any}): {[string]: any}
	local target, err = Utils.requireScript(data.path)
	if err then return err end

	if not data.edits or typeof(data.edits) ~= "table" or #data.edits == 0 then
		return {success = false, message = "Missing or empty edits array"}
	end

	local currentSource = ""
	pcall(function()
		currentSource = ScriptEditorService:GetEditorSource(target :: Instance)
	end)

	local editCount = 0
	local failedEdits = {}
	for i, edit in data.edits do
		if not edit.old_text or edit.old_text == "" then
			table.insert(failedEdits, "Edit " .. i .. ": missing old_text")
		elseif edit.new_text == nil then
			table.insert(failedEdits, "Edit " .. i .. ": missing new_text")
		else
			local startPos, endPos = currentSource:find(edit.old_text, 1, true)
			if not startPos then
				table.insert(failedEdits, "Edit " .. i .. ": text not found")
			else
				currentSource = currentSource:sub(1, startPos - 1) .. edit.new_text .. currentSource:sub(endPos + 1)
				editCount = editCount + 1
			end
		end
	end

	if editCount == 0 then
		return {success = false, message = "No edits applied: " .. table.concat(failedEdits, ", ")}
	end

	local editTarget = target :: Instance
	local editParent = editTarget.Parent
	local replacement = editTarget:Clone();
	(replacement :: any).Source = currentSource
	Utils.withRecording("Edit Script", function()
		replacement.Parent = editParent
		editTarget.Parent = nil
	end)

	Selection:Set({replacement})
	local msg = "Edited " .. editCount .. "/" .. #data.edits .. " in " .. replacement.Name
	if #failedEdits > 0 then
		msg = msg .. " (failed: " .. table.concat(failedEdits, ", ") .. ")"
	end
	return {success = true, message = msg}
end

return ScriptCmds


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local ScriptCmds = require(script.Parent.ScriptCmds)
local InstanceCmds = require(script.Parent.InstanceCmds)
local PropertyCmds = require(script.Parent.PropertyCmds)

local function run_luau(data: {[string]: any}): {[string]: any}
	local code = data.code
	if not code or code == "" then
		return {success = false, message = "Missing code"}
	end

	local moduleSource = [[
local __output = {}
local __totalChars = 0
local __MAX = 8000

local function print(...)
	local parts = {}
	for i = 1, select("#", ...) do
		parts[i] = tostring(select(i, ...))
	end
	local line = table.concat(parts, "\t")
	if __totalChars + #line <= __MAX then
		__totalChars = __totalChars + #line + 1
		table.insert(__output, line)
	end
end

local function warn(...)
	print(...)
end

]] .. code .. "\nreturn table.concat(__output, '\\n')"

	local tempMod = Instance.new("ModuleScript")
	tempMod.Source = moduleSource
	tempMod.Parent = ReplicatedStorage

	local ok, result = pcall(require, tempMod)
	tempMod:Destroy()

	if not ok then
		return {success = false, message = "Error: " .. tostring(result)}
	end
	return {success = true, message = "OK", data = {output = tostring(result or "")}}
end

local handlers = {
	run_luau = run_luau,
	read_script = ScriptCmds.read_script,
	create_script = ScriptCmds.create_script,
	update_script = ScriptCmds.update_script,
	edit_script = ScriptCmds.edit_script,
	create_instance = InstanceCmds.create_instance,
	create_ui = InstanceCmds.create_ui,
	rename = InstanceCmds.rename,
	move = InstanceCmds.move,
	delete = InstanceCmds.delete,
	clone = InstanceCmds.clone,
	insert_asset = InstanceCmds.insert_asset,
	set_property = PropertyCmds.set_property,
	set_properties = PropertyCmds.set_properties,
	add_tag = PropertyCmds.add_tag,
	remove_tag = PropertyCmds.remove_tag,
	set_attribute = PropertyCmds.set_attribute,
	undo = PropertyCmds.undo,
}

local function handleCommand(data: {[string]: any}): {[string]: any}
	local cmd = data.type

	if cmd == "ping" then
		return {success = true, message = "pong"}
	end

	local handler = handlers[cmd]
	if handler then
		return handler(data)
	end

	return {success = false, message = "Unknown command: " .. tostring(cmd)}
end

return handleCommand


local Config = {}

Config.PLUGIN_VERSION = "1.0.4"
Config.SERVER_URL = "wss://turnstile.metain.dev"
Config.MAX_RECONNECT_ATTEMPTS = 5

Config.state = {
	ws = nil :: WebStreamClient?,
	isConnected = false,
	isAuthorized = false,
	isReconnecting = false,
	reconnectToken = nil :: string?,
	lastUserId = nil :: string?,
	reconnectAttempts = 0,
	connectionId = 0,
	heartbeatThread = nil :: thread?,
	reconnectThread = nil :: thread?,
	reconnectResetThread = nil :: thread?,
}

return Config


local Root = script.Parent

local Config = require(Root.Config)
local UI = require(Root.UI)
local Connection = require(Root.Connection)

UI.init(plugin)

UI.toggleButton.Click:Connect(function()
	UI.widget.Enabled = not UI.widget.Enabled
end)

UI.connectBtn.MouseButton1Click:Connect(function()
	local state = Config.state
	if state.isConnected or state.isReconnecting then
		Connection.disconnect()
	else
		Connection.connect(UI.codeInput.Text)
	end
end)

UI.codeInput.FocusLost:Connect(function(enterPressed)
	local state = Config.state
	if enterPressed and not state.isConnected and not state.isReconnecting then
		Connection.connect(UI.codeInput.Text)
	end
end)


local Root = script:FindFirstAncestor("Metain")
local Config = require(Root.Config)

local UI = {}

local statusDot
local statusLabel
local connectBtn
local codeInput

local function getColor(styleColor: Enum.StudioStyleGuideColor): Color3
	return settings().Studio.Theme:GetColor(styleColor)
end

function UI.updateUI()
	local state = Config.state
	if state.isConnected and state.isAuthorized then
		statusDot.BackgroundColor3 = Color3.fromRGB(74, 222, 128)
		statusLabel.Text = "Connected"
		connectBtn.Text = "Disconnect"
		connectBtn.BackgroundColor3 = Color3.fromRGB(60, 60, 60)
		connectBtn.TextColor3 = Color3.fromRGB(200, 200, 200)
		codeInput.TextEditable = false
		codeInput.TextTransparency = 0.5
	elseif state.isConnected or state.isReconnecting then
		statusDot.BackgroundColor3 = Color3.fromRGB(251, 191, 36)
		statusLabel.Text = state.isReconnecting and "Reconnecting..." or "Connecting..."
		connectBtn.Text = "Cancel"
		connectBtn.BackgroundColor3 = Color3.fromRGB(60, 60, 60)
		connectBtn.TextColor3 = Color3.fromRGB(200, 200, 200)
		codeInput.TextEditable = false
		codeInput.TextTransparency = 0.5
	else
		statusDot.BackgroundColor3 = Color3.fromRGB(248, 113, 113)
		statusLabel.Text = "Not connected"
		connectBtn.Text = "Connect"
		connectBtn.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
		connectBtn.TextColor3 = Color3.fromRGB(20, 20, 20)
		codeInput.TextEditable = true
		codeInput.TextTransparency = 0
	end
end

function UI.init(pluginInstance)
	local toolbar = pluginInstance:CreateToolbar("Metain")
	UI.toggleButton = toolbar:CreateButton(
		"Metain",
		"Open Metain panel",
		"rbxassetid://93855725349422"
	)

	local widgetInfo = DockWidgetPluginGuiInfo.new(
		Enum.InitialDockState.Right,
		false,
		false,
		260, 220,
		220, 200
	)
	UI.widget = pluginInstance:CreateDockWidgetPluginGuiAsync("MetainPanel", widgetInfo)
	UI.widget.Title = "Metain"

	local frame = Instance.new("Frame")
	frame.Size = UDim2.fromScale(1, 1)
	frame.BorderSizePixel = 0
	frame.Parent = UI.widget

	local padding = Instance.new("UIPadding")
	padding.PaddingTop = UDim.new(0, 24)
	padding.PaddingBottom = UDim.new(0, 24)
	padding.PaddingLeft = UDim.new(0, 20)
	padding.PaddingRight = UDim.new(0, 20)
	padding.Parent = frame

	local layout = Instance.new("UIListLayout")
	layout.SortOrder = Enum.SortOrder.LayoutOrder
	layout.Padding = UDim.new(0, 16)
	layout.HorizontalAlignment = Enum.HorizontalAlignment.Center
	layout.VerticalAlignment = Enum.VerticalAlignment.Center
	layout.Parent = frame

	local statusFrame = Instance.new("Frame")
	statusFrame.Size = UDim2.new(0, 160, 0, 50)
	statusFrame.BackgroundTransparency = 1
	statusFrame.LayoutOrder = 1
	statusFrame.Parent = frame

	local statusLayout = Instance.new("UIListLayout")
	statusLayout.FillDirection = Enum.FillDirection.Vertical
	statusLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
	statusLayout.VerticalAlignment = Enum.VerticalAlignment.Center
	statusLayout.Padding = UDim.new(0, 6)
	statusLayout.Parent = statusFrame

	statusDot = Instance.new("Frame")
	statusDot.Size = UDim2.fromOffset(14, 14)
	statusDot.BackgroundColor3 = Color3.fromRGB(255, 100, 100)
	statusDot.BorderSizePixel = 0
	statusDot.LayoutOrder = 1
	statusDot.Parent = statusFrame

	local statusCorner = Instance.new("UICorner")
	statusCorner.CornerRadius = UDim.new(1, 0)
	statusCorner.Parent = statusDot

	statusLabel = Instance.new("TextLabel")
	statusLabel.Size = UDim2.fromOffset(160, 24)
	statusLabel.BackgroundTransparency = 1
	statusLabel.Text = "Not connected"
	statusLabel.TextXAlignment = Enum.TextXAlignment.Center
	statusLabel.TextSize = 18
	statusLabel.FontFace = Font.new("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.SemiBold)
	statusLabel.LayoutOrder = 2
	statusLabel.Parent = statusFrame

	local codeLabel = Instance.new("TextLabel")
	codeLabel.Size = UDim2.new(1, 0, 0, 18)
	codeLabel.BackgroundTransparency = 1
	codeLabel.Text = "Enter your connection code"
	codeLabel.TextXAlignment = Enum.TextXAlignment.Center
	codeLabel.TextSize = 12
	codeLabel.FontFace = Font.new("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.Regular)
	codeLabel.LayoutOrder = 2
	codeLabel.Parent = frame

	codeInput = Instance.new("TextBox")
	codeInput.Size = UDim2.new(1, 0, 0, 44)
	codeInput.Text = ""
	codeInput.PlaceholderText = "XXXXXX"
	codeInput.TextSize = 22
	codeInput.FontFace = Font.new("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.Bold)
	codeInput.ClearTextOnFocus = false
	codeInput.TextXAlignment = Enum.TextXAlignment.Center
	codeInput.LayoutOrder = 3
	codeInput.Parent = frame

	codeInput:GetPropertyChangedSignal("Text"):Connect(function()
		local clean = codeInput.Text:upper():gsub("[^A-Z0-9]", ""):sub(1, 6)
		if codeInput.Text ~= clean then
			codeInput.Text = clean
		end
	end)

	local inputCorner = Instance.new("UICorner")
	inputCorner.CornerRadius = UDim.new(0, 8)
	inputCorner.Parent = codeInput

	connectBtn = Instance.new("TextButton")
	connectBtn.Size = UDim2.new(1, 0, 0, 40)
	connectBtn.Text = "Connect"
	connectBtn.TextSize = 14
	connectBtn.FontFace = Font.new("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.Bold)
	connectBtn.LayoutOrder = 4
	connectBtn.AutoButtonColor = true
	connectBtn.Parent = frame

	local btnCorner = Instance.new("UICorner")
	btnCorner.CornerRadius = UDim.new(0, 8)
	btnCorner.Parent = connectBtn

	UI.codeInput = codeInput
	UI.connectBtn = connectBtn

	local function applyTheme()
		frame.BackgroundColor3 = getColor(Enum.StudioStyleGuideColor.MainBackground)
		statusLabel.TextColor3 = getColor(Enum.StudioStyleGuideColor.MainText)
		codeLabel.TextColor3 = getColor(Enum.StudioStyleGuideColor.SubText)
		codeInput.BackgroundColor3 = getColor(Enum.StudioStyleGuideColor.InputFieldBackground)
		codeInput.TextColor3 = getColor(Enum.StudioStyleGuideColor.MainText)
		codeInput.PlaceholderColor3 = getColor(Enum.StudioStyleGuideColor.DimmedText)
		codeInput.BorderSizePixel = 0
		connectBtn.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
		connectBtn.TextColor3 = Color3.fromRGB(20, 20, 20)
		connectBtn.BorderSizePixel = 0
	end

	applyTheme()
	settings().Studio.ThemeChanged:Connect(function()
		applyTheme()
		UI.updateUI()
	end)

	UI.updateUI()
end

return UI


local ChangeHistoryService = game:GetService("ChangeHistoryService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local ServerStorage = game:GetService("ServerStorage")
local StarterGui = game:GetService("StarterGui")
local StarterPlayer = game:GetService("StarterPlayer")

local Utils = {}

Utils.locationMap = {
	ServerScriptService = ServerScriptService,
	ServerStorage = ServerStorage,
	ReplicatedStorage = ReplicatedStorage,
	StarterGui = StarterGui,
	StarterPlayerScripts = StarterPlayer:WaitForChild("StarterPlayerScripts"),
	StarterCharacterScripts = StarterPlayer:WaitForChild("StarterCharacterScripts"),
	Workspace = workspace,
}

function Utils.findByPath(path: string): Instance?
	local segments = path:split(".")
	local current: Instance? = nil

	for i, segment in segments do
		if i == 1 then
			current = Utils.locationMap[segment] or game:FindFirstChild(segment)
		else
			if current then
				current = current:FindFirstChild(segment)
			end
		end
		if not current then return nil end
	end

	return current
end

function Utils.convertValue(value: any): any
	if typeof(value) ~= "table" then
		return value
	end

	local t = value._type
	if t == "UDim2" then
		local xScale = value.XScale or 0
		local xOffset = value.XOffset or 0
		local yScale = value.YScale or 0
		local yOffset = value.YOffset or 0
		return UDim2.new(xScale, xOffset, yScale, yOffset)
	elseif t == "Color3" then
		if value.Hex then
			return Color3.fromHex(value.Hex)
		else
			return Color3.fromRGB(value.R, value.G, value.B)
		end
	elseif t == "Vector2" then
		return Vector2.new(value.X, value.Y)
	elseif t == "Vector3" then
		return Vector3.new(value.X, value.Y, value.Z)
	elseif t == "UDim" then
		return UDim.new(value.Scale, value.Offset)
	elseif t == "CFrame" then
		local cf = CFrame.new(value.X, value.Y, value.Z)
		if value.RX or value.RY or value.RZ then
			cf = cf * CFrame.Angles(
				math.rad(value.RX or 0),
				math.rad(value.RY or 0),
				math.rad(value.RZ or 0)
			)
		end
		return cf
	elseif t == "Enum" then
		return Enum[value.EnumType][value.EnumItem]
	elseif t == "BrickColor" then
		if value.Name then
			return BrickColor.new(value.Name)
		elseif value.Number then
			return BrickColor.new(value.Number)
		end
	elseif t == "NumberRange" then
		return NumberRange.new(value.Min, value.Max)
	elseif t == "Rect" then
		return Rect.new(value.MinX, value.MinY, value.MaxX, value.MaxY)
	elseif t == "NumberSequence" then
		local keypoints = {}
		for _, kp in value.Keypoints do
			table.insert(keypoints, NumberSequenceKeypoint.new(kp.Time, kp.Value, kp.Envelope or 0))
		end
		return NumberSequence.new(keypoints)
	elseif t == "ColorSequence" then
		local keypoints = {}
		for _, kp in value.Keypoints do
			local color = Color3.fromRGB(kp.Color.R, kp.Color.G, kp.Color.B)
			table.insert(keypoints, ColorSequenceKeypoint.new(kp.Time, color))
		end
		return ColorSequence.new(keypoints)
	elseif t == "Font" then
		if value.EnumFont then
			return Font.fromEnum(Enum.Font[value.EnumFont])
		else
			local weight = value.Weight and Enum.FontWeight[value.Weight] or Enum.FontWeight.Regular
			local style = value.Style and Enum.FontStyle[value.Style] or Enum.FontStyle.Normal
			return Font.new(value.Family, weight, style)
		end
	end

	return value
end

function Utils.createFromSpec(spec: {[string]: any}): Instance
	local className = spec.ClassName
	if not className then
		error("Missing ClassName in spec")
	end

	local instance = Instance.new(className)

	for propName, propValue in spec do
		if propName == "ClassName" or propName == "Children" or propName == "Parent" then
			continue
		end

		pcall(function()
			(instance :: any)[propName] = Utils.convertValue(propValue)
		end)
	end

	if spec.Children then
		for _, childSpec in spec.Children do
			local child = Utils.createFromSpec(childSpec)
			child.Parent = instance
		end
	end

	return instance
end

function Utils.requireTarget(path: string): (Instance?, {[string]: any}?)
	local target = Utils.findByPath(path)
	if not target then
		return nil, {success = false, message = "Not found: " .. tostring(path)}
	end
	return target, nil
end

function Utils.requireScript(path: string): (Instance?, {[string]: any}?)
	local target = Utils.findByPath(path)
	if not target or not target:IsA("LuaSourceContainer") then
		return nil, {success = false, message = "Script not found: " .. tostring(path)}
	end
	return target, nil
end

function Utils.withRecording(label: string, fn: () -> ())
	local recording = ChangeHistoryService:TryBeginRecording("Metain: " .. label)
	local ok, err = pcall(fn)
	if recording then
		ChangeHistoryService:FinishRecording(recording,
			ok and Enum.FinishRecordingOperation.Commit
				or Enum.FinishRecordingOperation.Cancel)
	end
	if not ok then
		error(err)
	end
end

return Utils


local plugin = script:FindFirstAncestorWhichIsA("Plugin")

local Players = game:GetService("Players")
local ContentProvider = game:GetService("ContentProvider")
local UserService = game:GetService("UserService")
local MarketplaceService = game:GetService("MarketplaceService")
local HttpService = game:GetService("HttpService")

-- For crumbs
local GAME_ICON = { selected = "rbxassetid://18745845370", unselected = "rbxassetid://17650835923" }
local DATASTORE_ICON = { selected = "rbxassetid://18745840750", unselected = "rbxassetid://17650834077" }
local ORDERED_DATASTORE_ICON = { selected = "rbxassetid://18745847447", unselected = "rbxassetid://18467184552" }
local ALL_SCOPES_DATASTORE_ICON = { selected = "rbxassetid://18807229788", unselected = "rbxassetid://18807231111" }
local GLOBAL_DATASTORE_ICON = { selected = "rbxassetid://80462707681481", unselected = "rbxassetid://89897336645947" }
local KEY_ICON = { selected = "rbxassetid://18745602938", unselected = "rbxassetid://17650831214" }

local rfmt = require(script.Parent.rfmt)
local Types = require(script.Parent.Types)

local Settings = require(script.Parent.Settings)
local Session = require(script.Parent.Session)
local History = require(script.Parent.History).global

local Tree = require(script.Parent.UI.Viewers.Tree)

local Validators = require(script.Parent.Validators)
local PopupHelper = require(script.Parent.UI.PopupHelper)
local Transactor = require(script.Parent.Transactor)

local StyleStateHelper = require(script.Parent.UI.StyleState.StyleStateHelper)
local LoadingBarStyleState = require(script.Parent.UI.StyleState.Utility.LoadingBarStyleState)
local ConfirmDialogStyleState = require(script.Parent.UI.StyleState.Utility.ConfirmDialogStyleState)
local ContextMenuStyleState = require(script.Parent.UI.StyleState.Utility.ContextMenuStyleState)

local SingleKeyWidget = require(script.Parent.UI.Widgets.SingleKeyWidget)
local EditKeyView = require(script.Parent.UI.Views.EditKeyView)
local HistoryView = require(script.Parent.UI.Views.HistoryView)
local VersionsView = require(script.Parent.UI.Views.VersionsView)

local Theme = require(script.Parent.UI.Theme).global
local UI = require(script.Parent.UI)
local UIMessages = require(script.Parent.UI.UIMessages)
local ExportImportHelper = require(script.Parent.ExportImportHelper)

local constraintTextBoxStyleState = require(script.Parent.UI.Utilities.constrainTextBoxStyleState)
local disableFrameWhenModalAppears = require(script.Parent.UI.Utilities.disableFrameWhenModalAppears)

local SETTINGS_TWEEN_INFO = TweenInfo.new(0.2, Enum.EasingStyle.Cubic)
local WIDGET_INFO = DockWidgetPluginGuiInfo.new(
	Enum.InitialDockState.Float, -- Widget will be initialized in floating panel
	false, -- Widget will be initially enabled
	false, -- Don't override the previous enabled state
	500, -- Default width of the floating window
	350, -- Default height of the floating window
	350, -- Minimum width of the floating window
	230 -- Minimum height of the floating window
)

local gameName = game.Name
local gameNameDiscovered = Instance.new("BindableEvent")
task.spawn(function()
	pcall(function()
		local discoveredName = MarketplaceService:GetProductInfo(game.PlaceId).Name
		if discoveredName then
			gameName = discoveredName
			gameNameDiscovered:Fire()
		end
	end)
end)

local App = {}
App.__index = App

function App.new(id: string)
	local uiMessages = UIMessages.new(Theme)
	local self = setmetatable({
		id = id,
		session = Session.new(),
		ui = UI.new(Theme, uiMessages),
		uiMessages = uiMessages,

		editKeyView = nil,
		historyView = nil,
		viewer = nil,

		connections = {},

		_keyTransactor = Transactor.new(),

		_isListingKey = false,
		_isGettingKey = false,
		_isUpdatingKey = false,
	}, App)

	self.widget = plugin:CreateDockWidgetPluginGuiAsync(id, WIDGET_INFO)
	self.widget.Title = id
	self.widget.Name = id
	self.widget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling

	self.ui:setParent(self.widget)
	self.uiMessages:setParent(self.widget)

	self.historyView = HistoryView.from(Theme, History.global, self.ui.gui.ConnectDataStore.Content.History)

	-- Breadcrumbs

	self.ui.styleStates.breadcrumbs.crumbChanged:Connect(function(crumb)
		self.ui:setActiveView(crumb.category)
		self.ui:updateBreadcrumbsVisibility()
	end)

	self.ui:hideBreadcrumbs()

	-- Connect form

	self:_updateConnectButton("instant")
	self.ui.gui.ConnectDataStore.Content.Form.DataStoreName.TextBox:GetPropertyChangedSignal("Text"):Connect(function()
		self:_updateConnectButton()
	end)

	self.ui.styleStates.connect.dataStoreNameTextBox.textBox.FocusLost:Connect(function(entered, input)
		if input == nil then
			return
		end

		if entered then
			self:_submitConnectForm()
		end
	end)
	self.ui.styleStates.connect.dataStoreScopeTextBox.textBox.FocusLost:Connect(function(entered, input)
		if input == nil then
			return
		end

		if entered then
			self:_submitConnectForm()
		end
	end)

	constraintTextBoxStyleState(self.ui.styleStates.connect.dataStoreNameTextBox, { Validators.dataStoreName })
	constraintTextBoxStyleState(self.ui.styleStates.connect.dataStoreScopeTextBox, { Validators.dataStoreScope })

	self.ui.styleStates.connect.dataStoreNameTextBox.errorChanged:Connect(function()
		self:_updateConnectButton()
	end)
	self.ui.styleStates.connect.dataStoreScopeTextBox.errorChanged:Connect(function()
		self:_updateConnectButton()
	end)

	self.ui.styleStates.connect.dataStoreTypeSelect.changed:Connect(function()
		self.ui:updateConnectOptions()
		self:_updateConnectButton()
	end)

	self.ui.gui.ConnectDataStore.Content.Form.Connect.Connect.Activated:Connect(function()
		self:_submitConnectForm()
	end)

	-- History
	self.ui:updateHistoryTabVisible(#History.connectionRecords > 0)
	table.insert(self.connections, {
		History.connectionsChanged:Connect(function()
			self.ui:updateHistoryTabVisible(#History.connectionRecords > 0)
		end),
	})

	self.historyView.connectionRecordSelected:Connect(function(record: Types.DataStoreConnectionRecord)
		self:tryConnect(record.connection)
	end)
	self.historyView.connectionRecordDeleted:Connect(function(record: Types.DataStoreConnectionRecord)
		History:removeConnectionRecord(record.connection)
	end)
	self.historyView.connectionRecordPinChanged:Connect(
		function(record: Types.DataStoreConnectionRecord, pinned: boolean)
			if pinned then
				History:pinConnectionRecord(record.connection)
				self.uiMessages:success("Pinned connection.")
			else
				History:unpinConnectionRecord(record.connection)
				self.uiMessages:success("Unpinned connection.")
			end
		end
	)
	self.historyView.connectionRecordLoaded:Connect(function(record: Types.DataStoreConnectionRecord)
		self:_loadConnectionRecord(record)
		self.ui.styleStates.connect.tabs:select("Connect")
		self.ui.gui.ConnectDataStore.Content.CanvasPosition = Vector2.new(0, 0)
		self.uiMessages:success("Loaded connection.")
	end)

	-- DataStore listing

	self.ui.gui.ConnectDataStore.Content.DataStoreList.Table.LoadMore.Activated:Connect(function()
		if self.session.dataStorePages then
			if not self.session.dataStorePages.isFinished then
				local loadingBar = LoadingBarStyleState.from(
					Theme,
					self.ui.gui.ConnectDataStore.Content.DataStoreList.Table.LoadingBar
				)
				self.ui.gui.ConnectDataStore.Content.DataStoreList.Table.LoadingBar.Visible = true
				task.spawn(loadingBar.animate, loadingBar)

				self.ui.styleStates.connect.loadMore.buttonStyleState:setDisabled(true):update()
				local _, err = self.session.dataStorePages:increment()

				self.ui.styleStates.connect.loadMore.buttonStyleState:setDisabled(false):update()
				self.ui.gui.ConnectDataStore.Content.DataStoreList.Table.LoadingBar.Visible = false
				loadingBar:destroy()

				if err then
					self.uiMessages:reportDataStoreError(err)
					return
				end
			end
		end
	end)

	self.ui.dataStoreInfoSelected:Connect(function(info: DataStoreInfo)
		self:tryConnect({
			type = "normal",
			name = info.DataStoreName,
			scope = nil,
		})
	end)

	self.ui.gui.ConnectDataStore.Content.Form.Prefix.List.Activated:Connect(function()
		self:tryListDataStores()
	end)

	self.ui.gui.ConnectDataStore.Content.Form.Prefix.TextBox.FocusLost:Connect(function(enterPressed, input)
		if input == nil then
			return
		end
		if enterPressed then
			self:tryListDataStores()
		end
	end)

	-- Key Getting

	self.ui.styleStates.browse.key.textBox:GetPropertyChangedSignal("Text"):Connect(function()
		local text = self.ui.styleStates.browse.key.textBox.Text
		if #text == 0 then
			self.ui.styleStates.browse.key:setError(nil):update("veryFast")
		else
			local validator = if self.session.connection.type == "allScopes"
				then Validators.allScopesKey
				else Validators.dataStoreKey
			local valid, err = validator(text)
			if valid then
				self.ui.styleStates.browse.key:setError(nil):update("veryFast")
			else
				self.ui.styleStates.browse.key:setError(err):update("veryFast")
			end
		end
	end)

	self.ui.styleStates.browse.key.textBox.FocusLost:Connect(function(enterPressed, input)
		if input == nil then
			return
		end
		if not enterPressed then
			return
		end
		if not self.session then
			self.uiMessages:error("How are you on this page???")
			return
		end
		if self.ui.styleStates.browse.key.textBox.Text == "" then
			return
		end

		local loadingBar = LoadingBarStyleState.from(Theme, self.ui.styleStates.browse.key.textBox.LoadingBar)
		loadingBar.container.Visible = true
		self.ui.styleStates.browse.key:setDisabled(true):update()
		task.spawn(loadingBar.animate, loadingBar)

		local keyName = self.ui.styleStates.browse.key:getText(function(err: string)
			self.uiMessages:error(err)
		end)

		local success, err = self:_trySwapEditKeyView(keyName)

		loadingBar.container.Visible = false
		loadingBar:destroy()
		self.ui.styleStates.browse.key:setDisabled(false):update()

		if err then
			self.uiMessages:reportDataStoreError(err)
			return
		else
			self.ui.styleStates.browse.key.textBox.Text = ""
		end
	end)

	-- Key browsing

	self.ui.gui.BrowseKeys.Keys.Table.LoadMore.Activated:Connect(function()
		if self.session.keyPages then
			if not self.session.keyPages.isFinished then
				local loadingBar = LoadingBarStyleState.from(Theme, self.ui.gui.BrowseKeys.Keys.Table.LoadingBar)
				loadingBar.container.Visible = true
				task.spawn(loadingBar.animate, loadingBar)

				self.ui.styleStates.browse.loadMore.buttonStyleState:setDisabled(true):update()
				local _, err = self.session.keyPages:increment()

				self.ui.styleStates.browse.loadMore.buttonStyleState:setDisabled(false):update()
				loadingBar.container.Visible = false
				loadingBar:destroy()

				if err then
					self.uiMessages:reportDataStoreError(err)
					return
				end
			end
		end
	end)

	self.ui.gui.BrowseKeys.List.Activated:Connect(function()
		self:tryListKeys()
	end)

	self.ui.gui.BrowseKeys.Prefix.FocusLost:Connect(function(enterPressed, input)
		if input == nil then
			return
		end

		if enterPressed then
			self:tryListKeys()
		end
	end)

	self.ui.keySelected:Connect(function(key: DataStoreKey, openInNewWidget: boolean)
		if openInNewWidget then
			local forkedSession = self.session:fork()
			local widget, err = SingleKeyWidget.new({
				session = forkedSession,
				theme = Theme,
				keyName = key.KeyName,
			})

			if widget then
				widget.deleted:Connect(function()
					self:_onKeyDeleted(key.KeyName, forkedSession.connection)
				end)
			else
				self.uiMessages:reportDataStoreError(err)
			end
		else
			local success, err = self:_trySwapEditKeyView(key.KeyName)
			if not success then
				self.uiMessages:reportDataStoreError(err)
				return
			end
		end
	end)

	self.ui.idLookedUp:Connect(function(key: DataStoreKey)
		local id = tonumber(key.KeyName:match("%d+"))
		if not id then
			self.uiMessages:error("Invalid Id.")
			return
		end

		self:_tryLookupId(id)
	end)

	self.ui.keyDeleted:Connect(function(key: DataStoreKey)
		if self.session.currentKey == key.KeyName or self._keyTransactor.locked then
			return
		end

		local popupPosition = self.widget:GetRelativeMousePosition()
		local confirmed = ConfirmDialogStyleState.confirm(Theme, self.widget, popupPosition, {
			title = "Delete key?",
			message = "Are you sure you want to do this?",
		})

		if confirmed then
			if self.session.currentKey == key.KeyName and self._keyTransactor.locked then
				return
			end

			local confirmedAgain = ConfirmDialogStyleState.confirm(Theme, self.widget, popupPosition, {
				title = "Confirm again",
				message = `You are going to delete the key <b>{key.KeyName}</b>.`,
			})

			if confirmedAgain then
				local styleState = self.ui.styleStates.browse.loadMore:get(key.KeyName)
				styleState:setDisabled(true):update()

				local success, err = self.session:tryDeleteKey(key.KeyName)

				if not success then
					self.uiMessages:reportDataStoreError(err)
					styleState:setDisabled(false):update()
					return
				else
					self:_onKeyDeleted(key.KeyName, self.session.connection)
					self.uiMessages:success(`Deleted <b>{key.KeyName}</b>.`)
				end
			end
		end
	end)

	-- Ordered data store

	self.ui.orderedDataStoreView.listed:Connect(function()
		self:_tryListOrderedKeys()
	end)

	self.ui.orderedDataStoreView.orderChanged:Connect(function()
		self:_tryListOrderedKeys()
	end)

	self.ui.orderedDataStoreView.keyEdited:Connect(function(key, value)
		self:_wrapOrderedDataStoreOperation(function()
			local success, err = self.session:trySetOrderedKey(key, value)

			if not success then
				self.uiMessages:reportDataStoreError(err)
				task.wait()
			else
				self.ui.orderedDataStoreView:updateKey(key, value)
			end
		end)
	end)

	self.ui.orderedDataStoreView.keyAdded:Connect(function(key, value)
		self:_wrapOrderedDataStoreOperation(function()
			local success, err = self.session:trySetOrderedKey(key, value)

			if not success then
				self.uiMessages:reportDataStoreError(err)
				task.wait()
			else
				self.ui.orderedDataStoreView:addKey(key, value)
				self.uiMessages:success(`Key <b>{key}</b> added!`)
			end
		end)
	end)

	self.ui.orderedDataStoreView.keyDeleted:Connect(function(key)
		self:_wrapOrderedDataStoreOperation(function()
			local success, err = self.session:tryRemoveOrderedKey(key)
			if not success then
				self.uiMessages:reportDataStoreError(err)
				task.wait()
			else
				self.ui.orderedDataStoreView:deleteKey(key)
				self.uiMessages:success(`Key <b>{key}</b> deleted!`)
			end
		end)
	end)

	self.ui.orderedDataStoreView.keyQueried:Connect(function(key)
		self:_wrapOrderedDataStoreOperation(function()
			local value, err = self.session:tryGetOrderedKey(key)
			if err then
				self.uiMessages:reportDataStoreError(err)
				task.wait()
			else
				self.ui.orderedDataStoreView:setActiveKey(key, value)
			end
		end)
	end)

	self.ui.orderedDataStoreView.styleStates.loadMore.buttonStyleState.button.Activated:Connect(function()
		if self.session.keyPages then
			if not self.session.keyPages.isFinished then
				self:_wrapOrderedDataStoreOperation(function()
					local _, err = self.session.keyPages:increment()
					if err then
						self.uiMessages:reportDataStoreError(err)
					end
				end)
			end
		end
	end)

	-- Settings
	self.ui.styleStates.settingsButton.button.Activated:Connect(function()
		if self.ui.styleStates.settingsButton.selecting then
			self.ui.styleStates.settingsButton:setSelecting(false):update()
			StyleStateHelper.tween(self.ui.styleStates.settingsButton.button, SETTINGS_TWEEN_INFO, {
				Rotation = 0,
			})

			self.ui:updateBreadcrumbsVisibility()
			self.ui:setActiveView(self.ui.styleStates.breadcrumbs:getCurrentCrumb().category)
		else
			self.ui.styleStates.settingsButton:setSelecting(true):update()
			StyleStateHelper.tween(self.ui.styleStates.settingsButton.button, SETTINGS_TWEEN_INFO, {
				Rotation = 30,
			})

			self.ui.styleStates.breadcrumbs.frame.Visible = false
			self.ui:setActiveView("settings")
		end
		self.ui:updateSettingsButtonPosition()
	end)

	table.insert(
		self.connections,
		Settings.changed:Connect(function(name: string, value: any)
			if name == "hideGameName" then
				self.ui.styleStates.breadcrumbs:setCrumbNameVisibleSafe(1, not value)
			end
		end)
	)
	self.ui.styleStates.connect.tabs.selectingChanged:Connect(function(tab)
		self.ui:switchConnectTab(tab:lower())
		self.ui:autoFocus()

		if Settings.get("automaticallyList") then
			if tab:lower() == "list" then
				if not self.session.dataStorePages then
					self:tryListDataStores()
				end
			end
		end
	end)

	table.insert(
		self.connections,
		gameNameDiscovered.Event:Connect(function()
			self.ui.styleStates.breadcrumbs:renameCrumbSafe(1, gameName)
		end)
	)

	table.insert(
		self.connections,
		self._keyTransactor.lockedChanged:Connect(function(locked: boolean)
			self.ui.styleStates.breadcrumbs:setDisabled(locked):update()
		end)
	)

	table.insert(
		self.connections,
		self.ui.gui:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
			if self.ui.activeView == "game" then
				self.ui:_updateConnectCentering()
			end
		end)
	)

	-- Enabled
	self._enabledChanged = Instance.new("BindableEvent")
	self.enabledChanged = self._enabledChanged.Event
	self.widget:GetPropertyChangedSignal("Enabled"):Connect(function()
		self._enabledChanged:Fire(self.widget.Enabled)
	end)

	-- Init
	disableFrameWhenModalAppears(self.widget, { self.ui.gui })
	self.ui.styleStates.breadcrumbs:addCrumb(GAME_ICON, "game", gameName)
	self.ui.styleStates.breadcrumbs:setCrumbNameVisibleSafe(1, not Settings.get("hideGameName")) -- Hide the crumb name for the game if the setting says to
	task.defer(self.ui.updateSettingsButtonPosition, self.ui)

	-- load the first history item
	local loadingRecord = nil
	for _, v in History.global.connectionRecords do
		if v.pinned then
			loadingRecord = v
			break
		end
	end

	if not loadingRecord then
		loadingRecord = History.global.connectionRecords[1]
	end

	if loadingRecord then
		self:_loadConnectionRecord(loadingRecord, "instant")
	end

	return self
end

function App._updateConnectButton(self: App, transitionSpeed: StyleStateHelper.TransitionSpeed)
	local invalidName = (self.ui.styleStates.connect.dataStoreNameTextBox.textBox.Text == "")
		or self.ui.styleStates.connect.dataStoreNameTextBox.error
		or self.ui.styleStates.connect.dataStoreScopeTextBox.error
	local disabled = self.ui.styleStates.connect.dataStoreTypeSelect.selection:lower() ~= "global" and invalidName

	self.ui.styleStates.connect.connectButton:setDisabled(disabled):update(transitionSpeed)
end

function App.tryConnect(self: App, connection: Types.DataStoreConnection)
	if self._isListingKey then
		self.uiMessages:error(
			`Cannot connect to another DataStore while a {rfmt.code("ListKeysAsync")} request is being made.`
		)
		return
	end

	if connection.type == "global" then
		connection.name = "global"
	end

	local success, err = self.session:tryConnect(connection)
	if not success then
		self.uiMessages:reportDataStoreError(err)
		return
	end

	History:addConnection(connection)

	local crumbName = connection.name
	if connection.scope and connection.scope ~= "" then
		crumbName = `{connection.scope}/{connection.name}`
	end

	-- Reset ui
	if connection.type == "ordered" then
		self.ui.orderedDataStoreView:reset()
	else
		self.ui.gui.BrowseKeys.Keys.Empty.Visible = false
		self.ui.gui.BrowseKeys.Keys.Table.Visible = false
		self.ui.gui.BrowseKeys.Keys.Instructions.Visible = true
		self.ui.styleStates.browse.key.textBox.Text = ""
		self.ui.styleStates.browse.prefix.textBox.Text = ""
	end

	self.ui.styleStates.breadcrumbs:addCrumb(
		if connection.type == "allScopes"
			then ALL_SCOPES_DATASTORE_ICON
			elseif connection.type == "ordered" then ORDERED_DATASTORE_ICON
			elseif connection.type == "global" then GLOBAL_DATASTORE_ICON
			else DATASTORE_ICON,
		if connection.type == "ordered" then "orderedDatastore" else "datastore",
		crumbName
	)

	if Settings.get("automaticallyList") then
		if connection.type == "ordered" then
			if (not self.session.keyPages) and (self.session:hasBudgetFor("listOrdered")) then
				self:_tryListOrderedKeys()
			end
		else
			if (not self.session.keyPages) and (self.session:hasBudgetFor("list")) then
				self:tryListKeys()
			end
		end
	end
end

function App._submitConnectForm(self: App)
	local datastoreType = self.ui.styleStates.connect.dataStoreTypeSelect.selection

	local dataStoreName = self.ui.styleStates.connect.dataStoreNameTextBox:getText(function(err: string)
		self.uiMessages:error(err)
	end)
	local dataStoreScope = self.ui.styleStates.connect.dataStoreScopeTextBox:getText(function(err: string)
		self.uiMessages:error(err)
	end)
	if datastoreType:lower() ~= "global" and dataStoreName == "" then
		self.uiMessages:error("PUT A NAME!")
		return
	end
	if dataStoreScope == "" then
		dataStoreScope = nil
	end

	self.ui.gui.ConnectDataStore.Content.Form.DataStoreName.TextBox.Text = ""
	self.ui.gui.ConnectDataStore.Content.Form.DataStoreScope.TextBox.Text = ""

	self:tryConnect({
		type = self.ui.styleStates.connect.dataStoreTypeSelect.selection:gsub("^.", string.lower),
		name = dataStoreName,
		scope = dataStoreScope,
	})
end

function App.listDataStores(self: App, prefix: string?)
	local loadingBar =
		LoadingBarStyleState.from(Theme, self.ui.gui.ConnectDataStore.Content.Form.Prefix.TextBox.LoadingBar)
	loadingBar.container.Visible = true
	task.spawn(loadingBar.animate, loadingBar)
	self.ui.styleStates.connect.prefix:setDisabled(true):update()
	self.ui.styleStates.connect.listButton:setDisabled(true):update()

	local success, err = self.session:tryCreateDataStorePages(prefix)

	loadingBar.container.Visible = false
	loadingBar:destroy()
	self.ui.styleStates.connect.prefix:setDisabled(false):update()
	self.ui.styleStates.connect.listButton:setDisabled(false):update()

	if not success then
		self.uiMessages:reportDataStoreError(err)
		return
	end

	self.ui.styleStates.connect.loadMore:clear()
	self.session.dataStorePages:syncWithLoadMoreListStyleState(self.ui.styleStates.connect.loadMore)

	local empty = self.session.dataStorePages:isEmpty()
	self.ui.gui.ConnectDataStore.Content.DataStoreList.Table.Visible = not empty
	self.ui.gui.ConnectDataStore.Content.DataStoreList.Empty.Visible = empty
	self.ui.gui.ConnectDataStore.Content.DataStoreList.Instructions.Visible = false

	self.ui:_updateConnectCentering()
end

function App.tryListDataStores(self: App)
	local prefix = self.ui.styleStates.connect.prefix:getText(function(err)
		self.uiMessages:error(err)
	end)
	if prefix == "" then
		prefix = nil
	end

	self:listDataStores(prefix)
end

function App._trySwapEditKeyView(
	self: App,
	keyName: string,
	options: { version: string?, popCurrentCrumb: boolean?, session: Session.Session? }
): (boolean, string?)
	if self._isGettingKey then
		return false, "Already getting another key."
	end

	self._isGettingKey = true

	self.ui.styleStates.breadcrumbs:setDisabled(true):update()

	options = options or {}

	local success: boolean, err: string?
	local forkedSession = options.session or self.session:fork()
	if options.version then
		success, err = forkedSession:tryLoadVersion(keyName, options.version)
	else
		success, err = forkedSession:tryLoadKey(keyName)
	end

	task.wait()

	self._isGettingKey = false
	self.ui.styleStates.breadcrumbs:setDisabled(false):update()

	if not success then
		return false, err
	end

	local versionsView = nil

	if self.editKeyView then
		-- preserve the versions view if it is the same session
		if self.editKeyView.session == options.session then
			versionsView = self.editKeyView.versionsView
			versionsView:hide(true)
		else
			self.editKeyView.versionsView:destroy()
		end

		self.editKeyView:destroy()
	end

	if not versionsView then
		versionsView = VersionsView.new({
			theme = Theme,
			uiMessages = self.uiMessages,
			widget = self.widget,
			container = self.ui.gui.EditKey,
			session = forkedSession,
			loadVersion = function(version: string)
				local editKeyView = self.editKeyView
				if not editKeyView then return false end

				return editKeyView:handleLoadVersion(version)
			end,
		})
	end

	local editKeyView = EditKeyView.from({
		theme = Theme,
		uiMessages = self.uiMessages,
		frame = self.ui.gui.EditKey,
		inputReceiver = self.ui.gui.HotkeyInputReceiver,
		session = forkedSession,
		transactor = self._keyTransactor,
		versionsView = versionsView,
		swapCallback = function(keyName, version, session)
			return self:_trySwapEditKeyView(keyName, {
				popCurrentCrumb = true,
				version = version,
				session = session,
			})
		end,
		isVersionLoaded = not not options.version,
		-- if the session is preserved, they have the drawer open
		openWithVersionsView = not not options.session,
	})

	editKeyView.deleted:Connect(function()
		self.ui.styleStates.breadcrumbs:popCrumb()
		self:_onKeyDeleted(keyName, self.session.connection)
	end)

	editKeyView.idLookedUp:Connect(function(id)
		self:_tryLookupId(id)
	end)

	self.editKeyView = editKeyView

	if options.popCurrentCrumb then
		self.ui.styleStates.breadcrumbs:popCrumb({ silently = true })
	end
	self.ui.styleStates.breadcrumbs:addCrumb(KEY_ICON, "key", keyName)

	return true
end

function App._onKeyDeleted(self: App, keyName: string, connection: Types.DataStoreConnection)
	local isSameConnection = Session.areConnectionsSame(self.session.connection, connection)
	if not Settings.get("listDeletedKeys") then
		if isSameConnection then
			self.ui.styleStates.browse.loadMore:tryRemove(keyName)
		end
	end

	local lastCrumb = self.ui.styleStates.breadcrumbs.crumbs[#self.ui.styleStates.breadcrumbs.crumbs]
	-- NOTE: This will not work if the user quickly switches to AllScopes, reconnects to the DataStore, then gets the key (since the name will be different because AllScopes required the scope)
	if lastCrumb.category == "key" and (lastCrumb.name == keyName) and isSameConnection then
		if self.editKeyView then
			self.editKeyView:destroy()
			self.editKeyView = nil
		end

		self.ui.styleStates.breadcrumbs:popCrumb()
		self.session:resetKey()
	end
end

function App._loadConnectionRecord(self: App, record: Types.DataStoreConnection, transitionSpeed: StyleStateHelper.TransitionSpeed)
	local dataStoreType = record.connection.type:gsub("^.", string.upper)
	self.ui.styleStates.connect.dataStoreTypeSelect:setSelection(dataStoreType)
	self.ui.styleStates.connect.dataStoreNameTextBox.textBox.Text = if dataStoreType == "Global" then "" else record.connection.name
	self.ui.styleStates.connect.dataStoreScopeTextBox.textBox.Text = record.connection.scope or ""
	self.ui:updateConnectOptions()
	self:_updateConnectButton(transitionSpeed)
end

function App.listKeys(self: App, prefix: string?)
	if self._isListingKey then
		return
	end

	self._isListingKey = true

	local loadingBar = LoadingBarStyleState.from(Theme, self.ui.gui.BrowseKeys.Prefix.LoadingBar)
	loadingBar.container.Visible = true
	task.spawn(loadingBar.animate, loadingBar)
	self.ui.styleStates.browse.prefix:setDisabled(true):update()
	self.ui.styleStates.browse.listButton:setDisabled(true):update()

	local success, err = self.session:tryCreateKeyPages(prefix)

	loadingBar.container.Visible = false
	loadingBar:destroy()

	self.ui.styleStates.browse.prefix:setDisabled(false):update()
	self.ui.styleStates.browse.listButton:setDisabled(false):update()

	self._isListingKey = false

	if not success then
		self.uiMessages:reportDataStoreError(err)
		return
	end

	self.ui.styleStates.browse.loadMore:clear()
	self.session.keyPages:syncWithLoadMoreListStyleState(self.ui.styleStates.browse.loadMore)

	local empty = self.session.keyPages:isEmpty()
	self.ui.gui.BrowseKeys.Keys.Table.Visible = not empty
	self.ui.gui.BrowseKeys.Keys.Empty.Visible = empty
	self.ui.gui.BrowseKeys.Keys.Instructions.Visible = false
end

function App.tryListKeys(self: App)
	local prefix = self.ui.styleStates.browse.prefix:getText(function(err: string)
		self.uiMessages:error(err)
	end)
	if prefix == "" then
		prefix = nil
	end

	self:listKeys(prefix)
end

function App._wrapOrderedDataStoreOperation(self: App, func: () -> ())
	if self.ui.orderedDataStoreView.locked then
		return
	end

	self.ui.orderedDataStoreView:setLocked(true)
	self.ui.styleStates.breadcrumbs:setDisabled(true):update()

	local endLoadingBar = LoadingBarStyleState.show(Theme, self.ui.orderedDataStoreView.frame.LoadingBar)

	-- Main
	func()
	--

	task.delay(0, function()
		self.ui.orderedDataStoreView:setLocked(false)
	end)
	self.ui.styleStates.breadcrumbs:setDisabled(false):update()

	endLoadingBar()
end

function App._tryListOrderedKeys(self: App)
	self:_wrapOrderedDataStoreOperation(function()
		local success, err = self.session:tryCreateOrderedKeyPages(
			self.ui.orderedDataStoreView.order,
			self.ui.orderedDataStoreView.minValue,
			self.ui.orderedDataStoreView.maxValue
		)

		if not success then
			self.uiMessages:reportDataStoreError(err)
			task.wait() -- Wait for tweens so they don't clash with each other when updating... TODO: make this not necessary
		else
			self.ui.orderedDataStoreView:setActiveKey(nil)
			self.ui.orderedDataStoreView.styleStates.loadMore:clear()
			self.session.keyPages:syncWithLoadMoreListStyleState(self.ui.orderedDataStoreView.styleStates.loadMore)

			self.ui.orderedDataStoreView:setKeysState(if self.session.keyPages:isEmpty() then "empty" else "notEmpty")
		end
	end)
end

function App._tryLookupId(self: App, id: number)
	local info = nil
	local card = self.ui:createUserCard()
	local success, err = pcall(function()
		info = UserService:GetUserInfosByUserIdsAsync({ id })
	end)

	if not success then
		card:abort()
		self.uiMessages:error(err)
	elseif info[1] then
		local thumbnailUrl = nil
		local success, err = pcall(function()
			thumbnailUrl =
				Players:GetUserThumbnailAsync(id, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size100x100)
			local label = Instance.new("ImageLabel")
			label.Image = thumbnailUrl
			ContentProvider:PreloadAsync({ label })
		end)

		if not success then
			card:abort()
			self.uiMessages:error(err)
		else
			card:populate(thumbnailUrl, info[1])
		end
	else
		card:abort()
		self.uiMessages:error("User not found.")
	end
end

function App:toggleEnabled()
	self.widget.Enabled = not self.widget.enabled
	if self.widget.Enabled then
		self.ui:autoFocus()
	end
end

function App:getEnabled()
	return self.widget.Enabled
end

function App:destroy()
	for _, connection in self.connections do
		connection:Disconnect()
	end
	self._keyTransactor:destroy()
	self.ui:destroy()
end

export type App = typeof(App.new("DataDelve"))
return App