local ADDON_NAME, RPAL = ...
if type(RPAL) ~= "table" then return end

local function Settings()
    if not RPAL.GetSettings then return nil end
    local settings = RPAL.GetSettings()
    settings.keybinds = type(settings.keybinds) == "table" and settings.keybinds or {}
    return settings
end

local function SafeCall(fn, ...)
    if type(fn) ~= "function" then return false end
    local ok, result = pcall(fn, ...)
    return ok and result ~= false
end

local function OpenDesk(key, fallback)
    if type(RPAL.OpenDeskNavigation) == "function" then return SafeCall(RPAL.OpenDeskNavigation, key) end
    return SafeCall(fallback)
end

local function ToggleWalkRun()
    -- Use Blizzard's native walk/run toggle when it is exposed. RunBinding is
    -- retained as a compatibility fallback so the ODYN shortcut behaves like
    -- the stock "Toggle Run/Walk" binding without taking ownership of it.
    if type(_G.ToggleRun) == "function" then return SafeCall(_G.ToggleRun) end
    if type(_G.RunBinding) == "function" then return SafeCall(_G.RunBinding, "TOGGLERUN") end
    return false
end

RPAL.ODYN_KEYBIND_DEFS = {
    -- Quick Actions: these are intentionally first so Settings can present the
    -- hover workflow as one coherent group instead of burying it among menus.
    {id="quickaction", label="Interact", category="quick", special="quickaction", allowModifierOnly=true, overrideNative=true, action=function() return SafeCall(RPAL.QuickActionMenuHotkeyPressed) end},
    {id="quickinspect", label="Inspect Profile", category="quick", contextOnly=true, overrideNative=true, action=function() return SafeCall(RPAL.QuickActionInspectPressed) end},
    {id="hoverlog", label="Chat Log / IC-OOC", category="quick", special="hoverlog", contextOnly=true, overrideNative=true, action=function() return SafeCall(RPAL.QuickActionChatLogPressed) end},
    {id="quickwhisper", label="Whisper / Session", category="quick", contextOnly=true, overrideNative=true, action=function() return SafeCall(RPAL.QuickActionWhisperPressed) end},
    {id="quicktrp", label="Open TRP", category="quick", contextOnly=true, overrideNative=true, action=function() return SafeCall(RPAL.QuickActionTRPPressed) end},
    {id="lendear", label="Focus Messages", category="quick", contextOnly=true, overrideNative=true, action=function() return SafeCall(RPAL.ToggleLendEarForCurrentPanel) end},

    -- Navigation
    {id="main", label="Toggle ODYN", category="navigation", action=function() return SafeCall(RPAL.ToggleMainUI) end},
    {id="overview", label="Open Overview", category="navigation", action=function() return OpenDesk("overview") end},
    {id="persona", label="Open Persona Beta", category="navigation", action=function() return SafeCall(RPAL.OpenPersonalityUI) end},
    {id="atlas", label="Open Talent Atlas", category="navigation", action=function() return SafeCall(RPAL.ToggleTalentAtlas) or SafeCall(RPAL.OpenTalentAtlas) end},
    {id="sessions", label="Open Sessions", category="navigation", action=function() return OpenDesk("chronicle", function() return RPAL.OpenChronicle and RPAL.OpenChronicle("Scenes") end) end},
    {id="achievements", label="Open Achievements", category="navigation", action=function() return OpenDesk("achievements") end},
    {id="calendar", label="Open Calendar", category="navigation", action=function() return OpenDesk("calendar", RPAL.OpenCalendar) end},
    {id="seasons", label="Open Seasons", category="navigation", action=function() return SafeCall(RPAL.OpenSeasonsUI) end},
    {id="health", label="Open Health", category="navigation", action=function() return OpenDesk("injury", RPAL.OpenInjurySystem) end},
    {id="currencies", label="Open Currencies", category="navigation", action=function() return SafeCall(RPAL.OpenCurrencyUI) end},
    {id="skillset", label="Open Skill Set", category="navigation", action=function() return OpenDesk("skillset", RPAL.OpenSkillSet) end},
    {id="storyarcs", label="Open Story Arcs", category="navigation", action=function() return SafeCall(RPAL.OpenStoryArcBoard) end},
    {id="hearthstone", label="Open Unofficial Hearthstone", category="navigation", action=function() return SafeCall(RPAL.OpenHearthstone) end},
    {id="chess", label="Open Chess", category="navigation", action=function() return SafeCall(RPAL.OpenBoardGame, "chess", "play") end},
    {id="checkers", label="Open Checkers", category="navigation", action=function() return SafeCall(RPAL.OpenBoardGame, "checkers", "play") end},
    {id="customize", label="Open Customize", category="navigation", action=function() return OpenDesk("customize", RPAL.OpenCustomization) end},
    {id="tools", label="Open Tools", category="navigation", action=function() return OpenDesk("tools", RPAL.OpenTools) end},
    {id="dmkit", label="Open DM Kit", category="navigation", action=function() return OpenDesk("dmkit", RPAL.OpenDMKit) end},
    {id="settings", label="Open Settings", category="navigation", action=function() return SafeCall(RPAL.OpenSettings) end},

    -- Roleplay / chat-local utility
    -- Chat Speaker is intentionally special/contextual: ChatQoL consumes it
    -- only while a WoW chat edit box is active, so TAB remains untouched in
    -- normal gameplay.
    {id="chatspeaker", label="Cycle Self / Companions", category="roleplay", special="chatcontext", overrideNative=true},
    {id="trpstatus", label="Toggle TRP IC / OOC", category="roleplay", action=function() return SafeCall(RPAL.ToggleTRPICStatus) end},
    {id="walkrun", label="Toggle Walk / Run (WoW)", category="roleplay", action=ToggleWalkRun, nativeAction="TOGGLERUN"},
}

local defsByID = {}
for _, def in ipairs(RPAL.ODYN_KEYBIND_DEFS) do defsByID[def.id] = def end

local owner = CreateFrame("Frame", "ODYN_KeybindOwner", UIParent)
local focusOwner = CreateFrame("Frame", "ODYN_FocusMessagesBindingOwner", UIParent)
local quickActionOwner = CreateFrame("Frame", "ODYN_QuickActionBindingOwner", UIParent)
local buttons = {}
local pendingApply = false
local focusPanelWanted = false
local focusPanelInstalled = false
local focusPanelInstalledKey
local quickActionContextWanted = false
local quickActionContextInstalled = false
local quickActionContextInspectKey
local quickActionContextChatKey
local quickActionContextWhisperKey
local quickActionContextTRPKey

local function ButtonNameFor(id)
    return "ODYN_Keybind_" .. tostring(id):upper():gsub("[^A-Z0-9_]", "_")
end

local function EnsureActionButton(id, action)
    local button = buttons[id]
    if button then return button end
    local name = ButtonNameFor(id)
    button = CreateFrame("Button", name, UIParent)
    -- BindingClick requires a live button. Keep it technically shown but invisible and off-screen.
    button:SetSize(1, 1)
    button:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", -40, -40)
    button:SetAlpha(0)
    button:EnableMouse(false)
    button:RegisterForClicks("LeftButtonUp")
    button:Show()
    button:SetScript("OnClick", function()
        if RPAL._odynKeyCaptureActive then return end
        if type(action) == "function" then
            local ok, err = pcall(action)
            if not ok then print("|cffff7777ODYN:|r Keybind action failed: " .. tostring(err)) end
        end
    end)
    buttons[id] = button
    return button
end

for _, def in ipairs(RPAL.ODYN_KEYBIND_DEFS) do
    if not def.nativeAction and not def.special then EnsureActionButton(def.id, def.action) end
end
EnsureActionButton("hoverlog", defsByID.hoverlog and defsByID.hoverlog.action)
EnsureActionButton("quickaction", defsByID.quickaction and defsByID.quickaction.action)

local function ClearFocusMessagesPanelBinding()
    if not focusPanelInstalled then focusPanelInstalledKey=nil;return true end
    if InCombatLockdown and InCombatLockdown() then pendingApply=true;return false,"deferred" end
    if type(ClearOverrideBindings)~="function" then return false,"binding_api_unavailable" end
    local ok=pcall(ClearOverrideBindings,focusOwner)
    if ok then focusPanelInstalled=false;focusPanelInstalledKey=nil end
    return ok,ok and nil or"binding_clear_failed"
end

local function ReconcileFocusMessagesPanelBinding()
    local settings=Settings()
    local key=settings and tostring(settings.keybinds.lendear or""):upper()or""
    if not focusPanelWanted or key=="" then return ClearFocusMessagesPanelBinding() end
    if focusPanelInstalled and focusPanelInstalledKey==key then return true end
    if InCombatLockdown and InCombatLockdown() then pendingApply=true;return false,"deferred" end
    if type(SetOverrideBindingClick)~="function"or type(ClearOverrideBindings)~="function"then return false,"binding_api_unavailable"end

    -- This owner is deliberately separate from ODYN's normal shortcuts. It
    -- shadows the user's underlying WoW action only for the lifetime of the
    -- open Hover Log, then clearing it reveals that action unchanged.
    pcall(ClearOverrideBindings,focusOwner)
    focusPanelInstalled=false;focusPanelInstalledKey=nil
    local button=EnsureActionButton("lendear",defsByID.lendear and defsByID.lendear.action)
    local ok,result=pcall(SetOverrideBindingClick,focusOwner,true,key,button:GetName(),"LeftButton")
    if ok and result~=false then focusPanelInstalled=true;focusPanelInstalledKey=key;return true end
    pcall(ClearOverrideBindings,focusOwner)
    return false,"binding_failed"
end

function RPAL.SetFocusMessagesPanelBindingActive(active)
    focusPanelWanted=active==true
    return ReconcileFocusMessagesPanelBinding()
end

local function ClearQuickActionContextBindings()
    if not quickActionContextInstalled then
        quickActionContextInspectKey=nil;quickActionContextChatKey=nil;quickActionContextWhisperKey=nil;quickActionContextTRPKey=nil
        return true
    end
    if InCombatLockdown and InCombatLockdown() then pendingApply=true;return false,"deferred" end
    if type(ClearOverrideBindings)~="function" then return false,"binding_api_unavailable" end
    local ok=pcall(ClearOverrideBindings,quickActionOwner)
    if ok then
        quickActionContextInstalled=false
        quickActionContextInspectKey=nil
        quickActionContextChatKey=nil
        quickActionContextWhisperKey=nil
        quickActionContextTRPKey=nil
    end
    return ok,ok and nil or"binding_clear_failed"
end

local function ReconcileQuickActionContextBindings()
    local settings=Settings()
    local binds=settings and type(settings.keybinds)=="table" and settings.keybinds or {}
    local inspectKey=tostring(binds.quickinspect or "Q"):upper()
    local chatKey=settings and tostring(settings.hoverLogHotkey or "E"):upper() or "E"
    local whisperKey=tostring(binds.quickwhisper or "R"):upper()
    local trpKey=tostring(binds.quicktrp or "F"):upper()
    if not quickActionContextWanted then return ClearQuickActionContextBindings() end
    if inspectKey=="" and chatKey=="" and whisperKey=="" and trpKey=="" then return ClearQuickActionContextBindings() end
    if quickActionContextInstalled
        and quickActionContextInspectKey==inspectKey
        and quickActionContextChatKey==chatKey
        and quickActionContextWhisperKey==whisperKey
        and quickActionContextTRPKey==trpKey then return true end
    if InCombatLockdown and InCombatLockdown() then pendingApply=true;return false,"deferred" end
    if type(SetOverrideBindingClick)~="function" or type(ClearOverrideBindings)~="function" then return false,"binding_api_unavailable" end

    pcall(ClearOverrideBindings,quickActionOwner)
    quickActionContextInstalled=false
    quickActionContextInspectKey=nil
    quickActionContextChatKey=nil
    quickActionContextWhisperKey=nil
    quickActionContextTRPKey=nil

    local function BindContext(id,key)
        if key=="" then return true end
        local def=defsByID[id]
        local button=EnsureActionButton(id,def and def.action)
        local ok,result=pcall(SetOverrideBindingClick,quickActionOwner,true,key,button:GetName(),"LeftButton")
        return ok and result~=false
    end
    if not BindContext("quickinspect",inspectKey)
        or not BindContext("hoverlog",chatKey)
        or not BindContext("quickwhisper",whisperKey)
        or not BindContext("quicktrp",trpKey) then
        pcall(ClearOverrideBindings,quickActionOwner)
        return false,"binding_failed"
    end
    quickActionContextInstalled=true
    quickActionContextInspectKey=inspectKey
    quickActionContextChatKey=chatKey
    quickActionContextWhisperKey=whisperKey
    quickActionContextTRPKey=trpKey
    return true
end

function RPAL.SetQuickActionContextBindingActive(active)
    quickActionContextWanted=active==true
    return ReconcileQuickActionContextBindings()
end

local function IsModifierOnly(key)
    key = tostring(key or ""):upper()
    return key == "ALT" or key == "CTRL" or key == "SHIFT"
end

local function CurrentActionForKey(key)
    if type(GetBindingAction) ~= "function" or not key or key == "" then return "" end
    -- Ignore ODYN's runtime override when checking conflicts/cleaning legacy
    -- bindings; only inspect the user's underlying WoW binding set here.
    local ok, action = pcall(GetBindingAction, key, false)
    return ok and tostring(action or "") or ""
end

local function NativeWalkRunKeys()
    if type(GetBindingKey) ~= "function" then return {} end
    local ok, keys = pcall(function() return {GetBindingKey("TOGGLERUN")} end)
    if not ok or type(keys) ~= "table" then return {} end
    local out = {}
    for _, key in ipairs(keys) do
        key = tostring(key or "")
        if key ~= "" then out[#out + 1] = key:upper() end
    end
    return out
end

local function NativeWalkRunPrimaryKey()
    local keys = NativeWalkRunKeys()
    return keys[1] or ""
end

local function SetNativeWalkRunKey(key)
    key = tostring(key or ""):upper()
    if IsModifierOnly(key) then return false, "Use a normal key or a modifier + key combination." end
    if InCombatLockdown and InCombatLockdown() then
        return false, "The native WoW Walk / Run binding cannot be changed during combat."
    end
    if type(SetBinding) ~= "function" then return false, "WoW keybinding API is unavailable." end

    if key ~= "" then
        local current = CurrentActionForKey(key)
        if current ~= "" and current ~= "TOGGLERUN" then
            return false, "That key is already used by a WoW or ODYN binding. Clear it first or choose another key."
        end
    end

    local oldKeys = NativeWalkRunKeys()
    for _, oldKey in ipairs(oldKeys) do
        local current = CurrentActionForKey(oldKey)
        if current == "TOGGLERUN" then
            local ok, result = pcall(SetBinding, oldKey)
            if not ok or result == false then return false, "Could not clear the existing WoW Walk / Run binding." end
        end
    end

    if key ~= "" then
        local ok, result = pcall(SetBinding, key, "TOGGLERUN")
        if not ok or result == false then
            -- Best-effort rollback so a failed assignment does not leave the user unbound.
            for _, oldKey in ipairs(oldKeys) do pcall(SetBinding, oldKey, "TOGGLERUN") end
            return false, "Could not assign the WoW Walk / Run binding."
        end
    end

    if type(SaveBindings) == "function" then
        local set = type(GetCurrentBindingSet) == "function" and GetCurrentBindingSet() or 2
        local ok, result = pcall(SaveBindings, set or 2)
        if not ok or result == false then return false, "WoW could not save the Walk / Run binding." end
    end

    local settings = Settings()
    if settings and settings.keybinds then settings.keybinds.walkrun = nil end
    return true, key
end

local function OursFor(id, action)
    local buttonName = ButtonNameFor(id)
    return tostring(action or ""):find("CLICK " .. buttonName, 1, true) == 1
end

local function ClearPersistedODYNBinding(id, key)
    -- 1.4.8 and earlier used SetBindingClick, which could leave a CLICK action
    -- in WoW's saved binding set. Clear only ODYN-owned legacy entries before
    -- installing the runtime override; never touch an unrelated native bind.
    if not key or key=="" or IsModifierOnly(key) or type(SetBinding)~="function" then return true end
    local current=CurrentActionForKey(key)
    if OursFor(id,current) then
        local ok,result=pcall(SetBinding,key)
        return ok and result~=false
    end
    return true
end

local function ClearRuntimeKey(id, key)
    -- Override bindings are owned as one set by `owner`; rebuilding is cheap
    -- and happens only when the user changes a key or leaves combat.
    return ClearPersistedODYNBinding(id,key)
end

local function BindRuntimeKey(id, key)
    if not key or key=="" or IsModifierOnly(key) then return true end
    if type(SetOverrideBindingClick)~="function" then return false end
    local current=CurrentActionForKey(key)
    local def=defsByID[id]
    if current~="" and not OursFor(id,current) and not current:find("CLICK ODYN_Keybind_",1,true) and not (def and def.overrideNative) then
        return false
    end
    local button=EnsureActionButton(id,def and def.action)
    local ok,result=pcall(SetOverrideBindingClick,owner,true,key,button:GetName(),"LeftButton")
    return ok and result~=false
end

local function RebuildRuntimeBindings()
    if InCombatLockdown and InCombatLockdown() then pendingApply=true; return false,"deferred" end
    if type(ClearOverrideBindings)=="function" then pcall(ClearOverrideBindings,owner) end
    local settings=Settings()
    if not settings then return false,"settings_unavailable" end
    for _,def in ipairs(RPAL.ODYN_KEYBIND_DEFS) do
        if def.contextOnly then
            -- Context bindings (Quick Action rows / Focus Messages) intentionally
            -- shadow native WoW keys only while their panel is open. Clean stale
            -- persisted ODYN CLICK entries, but never install them globally.
            local key=(def.id=="hoverlog") and tostring(settings.hoverLogHotkey or "E"):upper() or tostring(settings.keybinds[def.id] or ""):upper()
            if key~="" then ClearPersistedODYNBinding(def.id,key) end
        elseif not def.nativeAction and not def.special then
            local key=tostring(settings.keybinds[def.id] or ""):upper()
            if key~="" then
                ClearPersistedODYNBinding(def.id,key)
                if not BindRuntimeKey(def.id,key) then return false,"binding_failed" end
            end
        end
    end
    local quickKey=tostring(settings.quickActionHotkey or "ALT"):upper()
    if quickKey~="" and not IsModifierOnly(quickKey) then
        ClearPersistedODYNBinding("quickaction",quickKey)
        if not BindRuntimeKey("quickaction",quickKey) then return false,"binding_failed" end
    end
    pendingApply=false
    local focusOK,focusErr=ReconcileFocusMessagesPanelBinding()
    if not focusOK and focusErr~="deferred"then return false,focusErr end
    local quickOK,quickErr=ReconcileQuickActionContextBindings()
    if not quickOK and quickErr~="deferred"then return false,quickErr end
    return true
end

local function SaveRuntimeBindings()
    if type(SaveBindings) ~= "function" then return true end
    local set = type(GetCurrentBindingSet) == "function" and GetCurrentBindingSet() or 2
    local ok, result = pcall(SaveBindings, set or 2)
    return ok and result ~= false
end

local function ApplyOne(id, key, oldKey)
    if InCombatLockdown and InCombatLockdown() then pendingApply=true; return true,"deferred" end
    if oldKey and oldKey~=key then ClearRuntimeKey(id,oldKey) end
    local ok,err=RebuildRuntimeBindings()
    -- Persist only possible cleanup of legacy CLICK bindings. ODYN's current
    -- shortcuts themselves live in SavedVariables + runtime overrides.
    SaveRuntimeBindings()
    return ok,err
end

local function ContextKeysMayShare(a,b)
    -- Focus Messages is active only inside the full Chat Log, while the
    -- contextual Quick Action keys exist only in the quick menu. Reusing E between Chat
    -- Log and Focus Messages is therefore safe and is the requested default.
    if a==b then return true end
    local quick={hoverlog=true,quickinspect=true,quickwhisper=true,quicktrp=true}
    if a=="lendear" and quick[b] then return true end
    if b=="lendear" and quick[a] then return true end
    return false
end

local function RemoveDuplicateKey(settings, key, exceptID)
    key=tostring(key or ""):upper()
    if key=="" then return end
    for id,bound in pairs(settings.keybinds or {}) do
        if id~=exceptID and tostring(bound or ""):upper()==key and not ContextKeysMayShare(exceptID,id) then
            settings.keybinds[id]=nil
            ClearRuntimeKey(id,bound)
        end
    end
    if exceptID~="hoverlog" and tostring(settings.hoverLogHotkey or ""):upper()==key and not ContextKeysMayShare(exceptID,"hoverlog") then
        local old=settings.hoverLogHotkey
        settings.hoverLogHotkey=""
        ClearRuntimeKey("hoverlog",old)
    end
    if exceptID~="quickaction" and tostring(settings.quickActionHotkey or ""):upper()==key then
        local old=settings.quickActionHotkey
        settings.quickActionHotkey=""
        ClearRuntimeKey("quickaction",old)
    end
end

function RPAL.GetODYNKeybindDefs()
    return RPAL.ODYN_KEYBIND_DEFS
end

function RPAL.GetODYNKeybind(id)
    if id=="walkrun" then return NativeWalkRunPrimaryKey() end
    if id=="hoverlog" then return RPAL.GetHoverLogHotkey() end
    if id=="quickaction" then return RPAL.GetQuickActionHotkey() end
    local settings=Settings()
    return settings and tostring(settings.keybinds[id] or "") or ""
end

function RPAL.SetODYNKeybind(id,key)
    local settings=Settings()
    local def=defsByID[id]
    if not settings or not def then return false,"unknown_binding" end
    key=tostring(key or ""):upper()
    if id=="walkrun" then return SetNativeWalkRunKey(key) end
    if id=="hoverlog" then return RPAL.SetHoverLogHotkey(key) end
    if id=="quickaction" then return RPAL.SetQuickActionHotkey(key) end
    if IsModifierOnly(key) then return false,"Use a normal key or a modifier + key combination for this action." end
    local oldKey=tostring(settings.keybinds[id] or "")
    if key~="" and not def.overrideNative then
        local current=CurrentActionForKey(key)
        if current~="" and not OursFor(id,current) and not current:find("CLICK ODYN_Keybind_",1,true) then
            return false,"That key is already used by a WoW binding. Clear it in WoW Keybindings first or choose another key."
        end
    end
    RemoveDuplicateKey(settings,key,id)
    settings.keybinds[id]=key~="" and key or nil
    local ok,err=ApplyOne(id,key,oldKey)
    if (id=="lendear" or id=="quickinspect" or id=="quickwhisper" or id=="quicktrp") and RPAL.RefreshHoverLogAppearance then pcall(RPAL.RefreshHoverLogAppearance) end
    if RPAL.RefreshHoverLogPrompt then pcall(RPAL.RefreshHoverLogPrompt) end
    return ok,err
end

function RPAL.GetHoverLogHotkey()
    local settings=Settings()
    return settings and tostring(settings.hoverLogHotkey or "E") or "E"
end

function RPAL.SetHoverLogHotkey(key)
    local settings=Settings()
    if not settings then return false,"settings_unavailable" end
    key=tostring(key or ""):upper()
    if IsModifierOnly(key) then return false,"Use a normal key or a modifier + key combination for Chat Log." end
    local oldKey=tostring(settings.hoverLogHotkey or "E"):upper()
    RemoveDuplicateKey(settings,key,"hoverlog")
    settings.hoverLogHotkey=key
    local ok,err=ApplyOne("hoverlog",key,oldKey)
    if RPAL.RefreshHoverLogAppearance then pcall(RPAL.RefreshHoverLogAppearance) end
    if RPAL.RefreshHoverLogPrompt then pcall(RPAL.RefreshHoverLogPrompt) end
    return ok,ok and key or err
end

function RPAL.GetQuickActionHotkey()
    local settings=Settings()
    return settings and tostring(settings.quickActionHotkey or "ALT") or "ALT"
end

function RPAL.SetQuickActionHotkey(key)
    local settings=Settings()
    if not settings then return false,"settings_unavailable" end
    key=tostring(key or ""):upper()
    local oldKey=tostring(settings.quickActionHotkey or "ALT"):upper()
    RemoveDuplicateKey(settings,key,"quickaction")
    settings.quickActionHotkey=key
    local ok,err=ApplyOne("quickaction",key,oldKey)
    if RPAL.RefreshHoverLogAppearance then pcall(RPAL.RefreshHoverLogAppearance) end
    if RPAL.RefreshHoverLogPrompt then pcall(RPAL.RefreshHoverLogPrompt) end
    return ok,ok and key or err
end

function RPAL.FormatODYNKey(key)
    key = tostring(key or "")
    if key == "" then return "Unbound" end
    local labels = {
        ALT="Alt", CTRL="Ctrl", SHIFT="Shift", TAB="Tab", SPACE="Space", ESCAPE="Esc",
        BACKSPACE="Backspace", DELETE="Delete", INSERT="Insert", HOME="Home", END="End",
        PAGEUP="Page Up", PAGEDOWN="Page Down", NUMPADPLUS="Numpad +", NUMPADMINUS="Numpad -", NUMPADDIVIDE="Numpad /", NUMPADMULTIPLY="Numpad *",
    }
    local parts = {}
    for token in key:gmatch("[^%-]+") do parts[#parts + 1] = labels[token] or token end
    return table.concat(parts, " + ")
end

local function CanonicalModifierKey(key)
    key = tostring(key or ""):upper()
    if key == "LALT" or key == "RALT" then return "ALT" end
    if key == "LCTRL" or key == "RCTRL" then return "CTRL" end
    if key == "LSHIFT" or key == "RSHIFT" then return "SHIFT" end
    return nil
end

local function ComposeKey(key)
    key = tostring(key or ""):upper()
    local modifierOnly = CanonicalModifierKey(key)
    if modifierOnly then return modifierOnly end
    local parts = {}
    if IsControlKeyDown and IsControlKeyDown() then parts[#parts + 1] = "CTRL" end
    if IsAltKeyDown and IsAltKeyDown() then parts[#parts + 1] = "ALT" end
    if IsShiftKeyDown and IsShiftKeyDown() then parts[#parts + 1] = "SHIFT" end
    parts[#parts + 1] = key
    return table.concat(parts, "-")
end

function RPAL.BeginODYNKeyCapture(id, button, allowModifierOnly, onDone)
    if not button or type(button.EnableKeyboard) ~= "function" then return false end
    if RPAL._odynKeyCaptureActive then return false, "already_capturing" end
    RPAL._odynKeyCaptureActive = true
    local oldText = button.label and button.label:GetText() or nil
    if button.label then button.label:SetText("PRESS KEY…") end
    button:EnableKeyboard(true)
    if button.SetPropagateKeyboardInput then button:SetPropagateKeyboardInput(false) end

    local finished = false
    local function Finish(savedKey, cancelled, errorText)
        if finished then return end
        finished = true
        RPAL._odynKeyCaptureActive = nil
        button:SetScript("OnKeyDown", nil)
        button:EnableKeyboard(false)
        if button.SetPropagateKeyboardInput then button:SetPropagateKeyboardInput(true) end
        if button.label then
            if cancelled then button.label:SetText(oldText or "Unbound")
            else button.label:SetText(RPAL.FormatODYNKey(savedKey)) end
        end
        if errorText then print("|cffff7777ODYN:|r " .. tostring(errorText)) end
        if type(onDone) == "function" then pcall(onDone, savedKey, cancelled, errorText) end
    end

    button:SetScript("OnKeyDown", function(_, key)
        key = tostring(key or ""):upper()
        if key == "ESCAPE" then Finish(nil, true); return end
        if key == "BACKSPACE" or key == "DELETE" then
            local ok, err
            if id == "hoverlog" then ok, err = RPAL.SetHoverLogHotkey("")
            elseif id == "quickaction" then ok, err = RPAL.SetQuickActionHotkey("")
            else ok, err = RPAL.SetODYNKeybind(id, "") end
            Finish("", false, ok and nil or err)
            return
        end
        local modifier = CanonicalModifierKey(key)
        if modifier and not allowModifierOnly then return end
        local composed = ComposeKey(key)
        local ok, err
        if id == "hoverlog" then ok, err = RPAL.SetHoverLogHotkey(composed)
        elseif id == "quickaction" then ok, err = RPAL.SetQuickActionHotkey(composed)
        else ok, err = RPAL.SetODYNKeybind(id, composed) end
        if ok then Finish(composed, false) else Finish(nil, true, err) end
    end)
    button:HookScript("OnHide", function() if not finished then Finish(nil, true) end end)
    return true
end

function RPAL.ApplyODYNKeybinds()
    local settings=Settings()
    if not settings then return false end
    if InCombatLockdown and InCombatLockdown() then pendingApply=true; return false end
    local ok,err=RebuildRuntimeBindings()
    SaveRuntimeBindings()
    return ok,err
end

function RPAL.ToggleTRPICStatus()
    local addon = _G.AddOn_TotalRP3
    local playerClass = type(addon) == "table" and addon.Player or nil
    local enums = type(addon) == "table" and addon.Enums or nil
    local statusEnum = type(enums) == "table" and enums.ROLEPLAY_STATUS or nil
    if type(playerClass) == "table" and type(playerClass.GetCurrentUser) == "function" and type(statusEnum) == "table" then
        local ok, user = pcall(playerClass.GetCurrentUser)
        if ok and type(user) == "table" and type(user.GetRoleplayStatus) == "function" and type(user.SetRoleplayStatus) == "function" then
            local good, current = pcall(user.GetRoleplayStatus, user)
            if good then
                local nextValue = current == statusEnum.OUT_OF_CHARACTER and statusEnum.IN_CHARACTER or statusEnum.OUT_OF_CHARACTER
                local setOK, err = pcall(user.SetRoleplayStatus, user, nextValue)
                if setOK then
                    local label = nextValue == statusEnum.IN_CHARACTER and "IC" or "OOC"
                    if RPAL.NotifyHoverLogTRPStatusChanged then pcall(RPAL.NotifyHoverLogTRPStatusChanged)
                    elseif label == "OOC" and RPAL.SetHoverLogArmed then RPAL.SetHoverLogArmed(false, true) end
                    print("|cff66e6ffODYN:|r TRP status → " .. label)
                    return true, label
                end
                return false, tostring(err)
            end
        end
    end

    -- Compatibility fallback for older TRP3 layouts exposing the active profile directly.
    local api = _G.TRP3_API
    local profileAPI = type(api) == "table" and api.profile or nil
    if type(profileAPI) == "table" and type(profileAPI.getPlayerCurrentProfile) == "function" then
        local ok, profile = pcall(profileAPI.getPlayerCurrentProfile)
        local character = ok and type(profile) == "table" and type(profile.player) == "table" and profile.player.character or nil
        if type(character) == "table" then
            local current = tonumber(character.RP)
            local nextValue = current == 2 and 1 or 2
            character.RP = nextValue
            character.v = ((tonumber(character.v) or 1) % 2) + 1
            local trp = _G.TRP3_Addon
            if type(trp) == "table" and type(trp.TriggerEvent) == "function" and type(trp.Events) == "table" and trp.Events.REGISTER_DATA_UPDATED then
                local playerID = type(api.globals) == "table" and api.globals.player_id or nil
                local profileID = type(profileAPI.getPlayerCurrentProfileID) == "function" and profileAPI.getPlayerCurrentProfileID() or nil
                pcall(trp.TriggerEvent, trp, trp.Events.REGISTER_DATA_UPDATED, playerID, profileID, "character")
            end
            local label = nextValue == 1 and "IC" or "OOC"
            if RPAL.NotifyHoverLogTRPStatusChanged then pcall(RPAL.NotifyHoverLogTRPStatusChanged)
            elseif label == "OOC" and RPAL.SetHoverLogArmed then RPAL.SetHoverLogArmed(false, true) end
            print("|cff66e6ffODYN:|r TRP status → " .. label)
            return true, label
        end
    end
    print("|cffff7777ODYN:|r TRP IC/OOC toggle is unavailable until Total RP 3 is loaded.")
    return false, "trp_unavailable"
end

local event = CreateFrame("Frame")
event:RegisterEvent("PLAYER_LOGIN")
event:RegisterEvent("PLAYER_REGEN_ENABLED")
event:SetScript("OnEvent", function(_, eventName)
    if eventName == "PLAYER_LOGIN" then
        if C_Timer and C_Timer.After then C_Timer.After(0.8, function() RPAL.ApplyODYNKeybinds() end)
        else RPAL.ApplyODYNKeybinds() end
    elseif pendingApply then
        RPAL.ApplyODYNKeybinds()
    end
end)
