--[[
    Commander 3.0 - Input Controller Component

    Handles the input EditBox with:
    - Keystroke handling via middleware chain
    - History navigation
    - Tab completion
    - Shortcuts (Ctrl+C, Ctrl+L, etc.)
]]

local ADDON_NAME, C3 = ...

-------------------------------------------------------------------------------
-- Input Controller Class
-------------------------------------------------------------------------------

local InputController = C3.class()
C3.UI = C3.UI or {}
C3.UI.InputController = InputController

---Create a new input controller
---@param parent frame Parent frame
---@param options? table
function InputController:init(parent, options)
    options = options or {}

    self.parent = parent
    self.session = options.session  -- Set later via SetSession() when tab is created
    self.terminal = options.terminal  -- Reference to terminal window for CastProgress

    -- Callbacks
    self.onCommand = options.onCommand
    self.onCancel = options.onCancel

    -- State
    self.historyBuffer = ""  -- Temp storage during history nav
    self.isNavigatingHistory = false
    self.currentInput = ""

    -- Intellisense preview state
    self._previewActive = false       -- Whether we're showing a preview
    self._previewOriginalText = ""    -- Original text before preview started
    self._previewOriginalCursor = 0   -- Original cursor position
    self._previewContext = nil        -- Context when preview started
    self._suppressTextChanged = false -- Suppress OnTextChanged during preview updates
    self._suppressEscapePressed = false -- Suppress OnEscapePressed after preview cancel
    self._suggestionTimer = nil       -- Debounce timer for suggestion requests

    -- Create the edit box
    self:_createEditBox()

    -- Middleware chain
    self._middleware = {}
    self:_setupDefaultMiddleware()

    -- Update prompt when player info becomes available
    self:_setupPromptUpdater()
end

-------------------------------------------------------------------------------
-- Class-Colored Prompt
-------------------------------------------------------------------------------

local CLASS_COLORS = {
    WARRIOR = "C79C6E", PALADIN = "F58CBA", HUNTER = "ABD473",
    ROGUE = "FFF569", PRIEST = "FFFFFF", DEATHKNIGHT = "C41F3B",
    SHAMAN = "0070DE", MAGE = "69CCF0", WARLOCK = "9482C9",
    MONK = "00FF96", DRUID = "FF7D0A", DEMONHUNTER = "A330C9",
    EVOKER = "33937F",
}

local function formatPromptPath(path)
    path = path or "/"

    -- Early return if VFS not available
    local vfs = C3:GetVFS()
    if not vfs or not vfs.driver or not vfs.driver.GetNode then
        return path:sub(-1) == "/" and path or path .. "/"
    end

    local display = path
    local node = vfs.driver:GetNode(path)
    if node and node.metadata and node.metadata.class then
        local classHex = CLASS_COLORS[node.metadata.class]
        if classHex then
            local name = C3.Paths.Filename(path)
            local parent = C3.Paths.Parent(path)
            local coloredName = string.format("|cFF%s%s|r", classHex, name)
            display = (parent == "/") and ("/" .. coloredName) or (parent .. "/" .. coloredName)
        end
    end

    if display ~= "/" and display:sub(-1) ~= "/" then
        display = display .. "/"
    end

    return display
end

local function tokenizeColorText(text)
    local tokens = {}
    local i = 1
    local len = #text

    while i <= len do
        local two = text:sub(i, i + 1)
        if two == "|c" and text:sub(i + 2, i + 9):match("^[0-9a-fA-F]+$") then
            table.insert(tokens, { type = "color", value = text:sub(i, i + 9) })
            i = i + 10
        elseif two == "|r" then
            table.insert(tokens, { type = "reset", value = "|r" })
            i = i + 2
        else
            table.insert(tokens, { type = "char", value = text:sub(i, i) })
            i = i + 1
        end
    end

    return tokens
end

local function visibleLength(text)
    local count = 0
    for _, token in ipairs(tokenizeColorText(text)) do
        if token.type == "char" then
            count = count + 1
        end
    end
    return count
end

local function truncateWithColors(text, maxLen)
    if not text or text == "" then return text end
    if visibleLength(text) <= maxLen then return text end
    if maxLen <= 3 then return "..." end

    local tokens = tokenizeColorText(text)
    local totalVisible = 0
    for _, token in ipairs(tokens) do
        if token.type == "char" then
            totalVisible = totalVisible + 1
        end
    end

    local target = maxLen - 3
    local startVisible = totalVisible - target + 1

    local currentColor = nil
    local startColor = nil
    local visibleIndex = 0
    local suffixTokens = {}
    local activeColor = nil

    for _, token in ipairs(tokens) do
        if token.type == "color" then
            currentColor = token.value
            if visibleIndex >= startVisible then
                table.insert(suffixTokens, token.value)
                activeColor = token.value
            end
        elseif token.type == "reset" then
            currentColor = nil
            if visibleIndex >= startVisible then
                table.insert(suffixTokens, token.value)
                activeColor = nil
            end
        else
            visibleIndex = visibleIndex + 1
            if visibleIndex == startVisible then
                startColor = currentColor
            end
            if visibleIndex >= startVisible then
                table.insert(suffixTokens, token.value)
            end
        end
    end

    local suffix = table.concat(suffixTokens)
    if startColor then
        suffix = startColor .. suffix
        if not activeColor then
            activeColor = startColor
        end
    end

    if activeColor then
        suffix = suffix .. "|r"
    end

    return "..." .. suffix
end

---Get the class-colored prompt string
---@return string
function InputController:GetPromptString()
    local name = UnitName("player") or "Player"
    local _, classFilename = UnitClass("player")
    local color = CLASS_COLORS[classFilename] or "FFFFFF"
    if self.session and self.session.ShowCwdPrompt and self.session:ShowCwdPrompt() then
        local vfs = C3:GetVFS()
        local cwd = truncateWithColors(formatPromptPath(vfs:GetCwd()), 30)
        return string.format("|cFF%s%s|r ❭ ", color, cwd)
    end
    return string.format("|cFF%s%s|r ❭ ", color, name)
end

---Update the prompt display and adjust text insets
function InputController:UpdatePrompt()
    local promptText = self:GetPromptString()
    self.prompt:SetText(promptText)
    -- Defer width calculation by 1 frame for accurate measurement
    C_Timer.After(0, function()
        if self.prompt and self.editBox then
            local width = self.prompt:GetStringWidth() + 8
            self.editBox:SetTextInsets(width, 4, 0, 0)
        end
    end)
end

---Setup event handler to update prompt when player info is available
function InputController:_setupPromptUpdater()
    local self_ref = self
    local eventFrame = CreateFrame("Frame")
    eventFrame:RegisterEvent("PLAYER_LOGIN")
    eventFrame:SetScript("OnEvent", function()
        self_ref:UpdatePrompt()
    end)
    -- Also try updating now in case we're already logged in
    if UnitName("player") then
        self:UpdatePrompt()
    end
end

-------------------------------------------------------------------------------
-- Edit Box Creation
-------------------------------------------------------------------------------

function InputController:_createEditBox()
    self.editBox = CreateFrame("EditBox", nil, self.parent)
    self.editBox:SetPoint("BOTTOMLEFT", 8, 8)
    self.editBox:SetPoint("BOTTOMRIGHT", -8, 8)

    -- Font and height (use theme with zoom support)
    local Theme = C3.UI.Theme
    local font = Theme and Theme.Font or "Fonts\\FRIZQT__.TTF"
    local fontSize = Theme and Theme:GetEffectiveFontSize() or 12
    local inputHeight = Theme and Theme:GetEffectiveInputHeight() or 20
    self.editBox:SetHeight(inputHeight)
    self.editBox:SetFont(font, fontSize, "")
    self.editBox:SetTextColor(1, 1, 1)

    -- Behavior
    self.editBox:SetAutoFocus(false)
    self.editBox:SetHistoryLines(0) -- We handle history ourselves
    self.editBox:SetMaxLetters(1024)
    self.editBox:SetMultiLine(false)

    -- Visual - minimal insets (will be adjusted by UpdatePrompt)
    self.editBox:SetTextInsets(4, 4, 0, 0)

    -- No background or border - input appears inline with output

    -- Combat lockdown indicator - shows visual cue when secure commands are blocked
    self:_setupCombatIndicator()

    -- Prompt indicator (class-colored "Playername > ")
    self.prompt = self.editBox:CreateFontString(nil, "OVERLAY")
    self.prompt:SetFont(font, fontSize, "")
    self.prompt:SetPoint("LEFT", 5, 0)
    -- Prompt text and insets will be set by UpdatePrompt() after player info is available

    -- Event handlers
    self.editBox:SetScript("OnEnterPressed", function()
        self:_onEnterPressed()
    end)

    self.editBox:SetScript("OnEscapePressed", function()
        self:_onEscapePressed()
    end)

    self.editBox:SetScript("OnKeyDown", function(_, key)
        self:_onKeyDown(key)
    end)

    self.editBox:SetScript("OnTabPressed", function()
        self:_onTabPressed()
    end)

    self.editBox:SetScript("OnTextChanged", function(_, userInput)
        if userInput then
            self:_onTextChanged()
        end
    end)

    self.editBox:SetScript("OnEditFocusGained", function()
        self:_onFocusGained()
    end)

    self.editBox:SetScript("OnEditFocusLost", function()
        self:_onFocusLost()
    end)

    self.editBox:SetScript("OnKeyUp", function(_, key)
        self:_onKeyUp(key)
    end)

    self.editBox:SetScript("OnChar", function(_, char)
        self:_onChar(char)
    end)
end

-------------------------------------------------------------------------------
-- Combat Lockdown Indicator
-------------------------------------------------------------------------------

---Setup combat lockdown visual indicator
---Shows red border when InCombatLockdown() prevents secure commands
function InputController:_setupCombatIndicator()
    local self_ref = self

    -- Create event frame for combat tracking
    local eventFrame = CreateFrame("Frame")
    eventFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
    eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
    eventFrame:SetScript("OnEvent", function(_, event)
        if event == "PLAYER_REGEN_DISABLED" then
            self_ref:_onCombatLockdown(true)
        elseif event == "PLAYER_REGEN_ENABLED" then
            self_ref:_onCombatLockdown(false)
        end
    end)

    -- Check initial state
    if InCombatLockdown() then
        self:_onCombatLockdown(true)
    end
end

---Handle combat lockdown state change
---@param inCombat boolean
function InputController:_onCombatLockdown(inCombat)
    if not self.prompt then return end

    if inCombat then
        -- Dim prompt to indicate secure commands are blocked
        self.prompt:SetAlpha(0.5)
    else
        -- Normal prompt
        self.prompt:SetAlpha(1.0)
        -- Clear combat-blocked indicator when leaving combat
        self:_setCombatBlockedIndicator(false)
    end
end

---Set visual indicator when combat blocks a secure action binding update
---This warns the user that their typed command won't execute as expected
---@param blocked boolean True to show red warning, false for normal state
function InputController:_setCombatBlockedIndicator(blocked)
    if not self.prompt then return end

    if blocked then
        -- Red prompt text to warn user
        self.prompt:SetTextColor(1, 0.2, 0.2)
        self._combatBlocked = true
    elseif self._combatBlocked then
        -- Restore class-colored prompt
        local _, classFilename = UnitClass("player")
        local colorHex = CLASS_COLORS[classFilename] or "FFFFFF"
        local r = tonumber(colorHex:sub(1, 2), 16) / 255
        local g = tonumber(colorHex:sub(3, 4), 16) / 255
        local b = tonumber(colorHex:sub(5, 6), 16) / 255
        self.prompt:SetTextColor(r, g, b)
        self._combatBlocked = false
    end
end

-------------------------------------------------------------------------------
-- Middleware System
-------------------------------------------------------------------------------

---Add middleware to the input chain
---@param name string
---@param handler function(key, ctrl, shift, alt) -> boolean (true = handled)
---@param position? number Optional position (1 = first/highest priority)
function InputController:AddMiddleware(name, handler, position)
    local mw = {
        name = name,
        handler = handler,
    }
    if position then
        table.insert(self._middleware, position, mw)
    else
        table.insert(self._middleware, mw)
    end
end

---Remove middleware by name
---@param name string
function InputController:RemoveMiddleware(name)
    for i, mw in ipairs(self._middleware) do
        if mw.name == name then
            table.remove(self._middleware, i)
            return
        end
    end
end

---Set read-only mode (disables text input but keeps key capture for prompts)
---@param readonly boolean
---@param suppressEscape boolean? If true, suppress the next OnEscapePressed event
function InputController:SetReadOnly(readonly, suppressEscape)
    self._readOnly = readonly
    if readonly then
        -- Store original text to restore later
        self._readOnlyText = self.editBox:GetText()
        self.editBox:SetText("")
        -- Keep keyboard enabled for Y/N capture, cursor will still show but that's ok
    else
        -- Exiting read-only: suppress the next character (from Y/N keypress)
        self._suppressNextChar = true
        self._readOnlyText = nil
        self.editBox:SetText("")
        -- Optionally suppress escape (when prompt was cancelled with ESC)
        if suppressEscape then
            self._suppressEscapePressed = true
        end
        self:UpdatePrompt()
    end
end

---Check if in read-only mode
---@return boolean
function InputController:IsReadOnly()
    return self._readOnly == true
end

---Process key through middleware chain
---@param key string
---@return boolean handled
function InputController:_processMiddleware(key)
    local ctrl = IsControlKeyDown()
    local shift = IsShiftKeyDown()
    local alt = IsAltKeyDown()

    for _, mw in ipairs(self._middleware) do
        local handled = mw.handler(key, ctrl, shift, alt)
        if handled then
            return true
        end
    end

    return false
end

---Setup default middleware
function InputController:_setupDefaultMiddleware()
    local self_ref = self  -- Closure reference

    -- Secure action middleware removed - using SecureHandlerWrapScript instead
    -- (see _setupSecureEnterHandler below)

    -- 1. Ctrl shortcuts
    self:AddMiddleware("ctrl_shortcuts", function(key, ctrl, shift, alt)
        if not ctrl then return false end

        -- Tab management shortcuts (check terminal.tabs first)
        local tabs = self_ref.terminal and self_ref.terminal.tabs

        if key == "T" and not shift then
            -- Ctrl+T: New tab
            if tabs then
                tabs:CreateTab({ activate = true })
            end
            return true
        elseif key == "W" then
            -- Ctrl+W: Close current tab, or close window if only one tab
            if tabs then
                if tabs:GetTabCount() > 1 then
                    local activeTab = tabs:GetActiveTab()
                    if activeTab then
                        tabs:CloseTab(activeTab)
                    end
                else
                    -- Only one tab - close the window
                    if self.terminal then
                        self.terminal:Close()
                    end
                end
            end
            return true
        elseif key == "TAB" then
            -- Ctrl+Tab / Ctrl+Shift+Tab: Next/Previous tab
            if tabs and tabs:GetTabCount() > 1 then
                local allTabs = tabs:GetTabs()
                local activeTab = tabs:GetActiveTab()
                local currentIndex = 1
                for i, tab in ipairs(allTabs) do
                    if tab == activeTab then
                        currentIndex = i
                        break
                    end
                end

                local newIndex
                if shift then
                    -- Previous tab
                    newIndex = currentIndex - 1
                    if newIndex < 1 then newIndex = #allTabs end
                else
                    -- Next tab
                    newIndex = currentIndex + 1
                    if newIndex > #allTabs then newIndex = 1 end
                end

                tabs:SelectTab(allTabs[newIndex])
            end
            return true
        elseif tonumber(key) and tonumber(key) >= 1 and tonumber(key) <= 9 then
            -- Ctrl+1-9: Switch to tab by number
            local num = tonumber(key)
            if tabs then
                local allTabs = tabs:GetTabs()
                if allTabs[num] then
                    tabs:SelectTab(allTabs[num])
                end
            end
            return true
        elseif key == "L" then
            -- Clear screen
            if self.onClear then self.onClear() end
            return true
        elseif key == "C" then
            -- Cancel / clear input
            self.editBox:SetText("")
            return true
        elseif key == "U" then
            -- Clear line before cursor
            local pos = self.editBox:GetCursorPosition()
            local text = self.editBox:GetText()
            self.editBox:SetText(text:sub(pos + 1))
            self.editBox:SetCursorPosition(0)
            return true
        elseif key == "K" then
            -- Clear line after cursor
            local pos = self.editBox:GetCursorPosition()
            local text = self.editBox:GetText()
            self.editBox:SetText(text:sub(1, pos))
            return true
        elseif key == "A" then
            -- Move to beginning
            self.editBox:SetCursorPosition(0)
            return true
        elseif key == "E" then
            -- Move to end
            self.editBox:SetCursorPosition(#self.editBox:GetText())
            return true
        elseif key == "=" or key == "+" or key == "NUMPADPLUS" or key == "PLUS" then
            -- Ctrl+=/+: Zoom in (supports ANSI, ISO, and numpad layouts)
            if C3.UI.Theme then
                C3.UI.Theme:ZoomIn()
            end
            return true
        elseif key == "-" or key == "NUMPADMINUS" or key == "MINUS" then
            -- Ctrl+-: Zoom out (supports all keyboard layouts)
            if C3.UI.Theme then
                C3.UI.Theme:ZoomOut()
            end
            return true
        elseif key == "0" or key == "NUMPAD0" then
            -- Ctrl+0: Reset zoom to default
            if C3.UI.Theme then
                C3.UI.Theme:ZoomReset()
            end
            return true
        end

        return false
    end)

    -- 2. History navigation
    self:AddMiddleware("history", function(key, ctrl, shift, alt)
        if key == "UP" then
            self:_navigateHistory(-1)
            return true
        elseif key == "DOWN" then
            self:_navigateHistory(1)
            return true
        end
        return false
    end)

    -- 3. Page navigation (when focused)
    self:AddMiddleware("page_nav", function(key, ctrl, shift, alt)
        if key == "PAGEUP" then
            if self.onPageUp then self.onPageUp() end
            return true
        elseif key == "PAGEDOWN" then
            if self.onPageDown then self.onPageDown() end
            return true
        end
        return false
    end)
end

-------------------------------------------------------------------------------
-- Toggle Keybind Detection
-------------------------------------------------------------------------------

---Check if the pressed key matches any Commander keybind
---@param key string The key that was pressed
---@return boolean
function InputController:_isCommanderKeybind(key)
    -- Build the current key combo string (matching WoW's format)
    local combo = ""
    if IsControlKeyDown() then combo = combo .. "CTRL-" end
    if IsAltKeyDown() then combo = combo .. "ALT-" end
    if IsShiftKeyDown() then combo = combo .. "SHIFT-" end
    combo = combo .. key

    -- All Commander keybinds to check
    local bindings = {
        "COMMANDER3_TOGGLE",
        "COMMANDER3_NEW_WINDOW",
        "COMMANDER3_FOCUS_LAST",
        "COMMANDER3_DROP_FOCUS",
        "COMMANDER3_TOGGLE_FOCUS",
    }

    for _, binding in ipairs(bindings) do
        local key1, key2 = GetBindingKey(binding)
        if combo == key1 or combo == key2 then
            return true
        end
    end

    return false
end

-------------------------------------------------------------------------------
-- Event Handlers
-------------------------------------------------------------------------------

function InputController:_onEnterPressed()
    -- Check if Enter was already handled (e.g., by suggestions dropdown)
    if self._suppressEnterPressed then
        self._suppressEnterPressed = false
        return
    end

    -- Note: If SecureAction was active, it was handled in OnKeyDown
    -- and this handler won't fire (focus was cleared)

    local text = self.editBox:GetText()
    text = text:match("^%s*(.-)%s*$") -- Trim

    if text == "" then
        return
    end

    -- Add to history
    if self.session then
        self.session:AddHistory(text)
    end
    self.isNavigatingHistory = false

    -- Clear input
    self.editBox:SetText("")

    -- Fire callback (executes the command)
    if self.onCommand then
        self.onCommand(text)
    end
end

function InputController:_onEscapePressed()
    -- Check if Escape was already handled (e.g., by preview cancel or prompt)
    if self._suppressEscapePressed then
        self._suppressEscapePressed = false
        return
    end

    if self.editBox:GetText() ~= "" then
        -- Clear input first
        self.editBox:SetText("")
        self.isNavigatingHistory = false
    else
        -- Fire cancel callback
        if self.onCancel then
            self.onCancel()
        else
            -- Default: clear focus
            self.editBox:ClearFocus()
        end
    end
end

-- Map special key names to their character equivalents
local KEY_TO_CHAR = {
    SPACE = " ",
}

-- Modifier keys that shouldn't stop pre-held filtering
local MODIFIER_KEYS = {
    LSHIFT = true, RSHIFT = true,
    LCTRL = true, RCTRL = true,
    LALT = true, RALT = true,
}

---Convert a key name to its character (for pre-held key filtering)
local function keyToChar(key)
    return KEY_TO_CHAR[key] or key:lower()
end

function InputController:_onKeyDown(key)
    -- Track this as a fresh key press - its character should be allowed through
    if self._filteringPreHeldChars then
        -- If this is a non-modifier key, stop filtering entirely
        -- This handles shifted characters (Shift+2 = @) that don't match key names
        if not MODIFIER_KEYS[key] then
            self._filteringPreHeldChars = false
            self._preHeldChars = nil
            self._freshKeyChars = nil
        end
    end

    -- Check if this key is bound to a Commander keybind - if so, let it through
    if self:_isCommanderKeybind(key) then
        -- SetPropagateKeyboardInput is protected - cannot call during combat
        if not InCombatLockdown() then
            self.editBox:SetPropagateKeyboardInput(true)
        end
        -- Clear focus immediately to prevent printable keybinds (like backtick)
        -- from typing a character before the window closes
        self.editBox:ClearFocus()
        return
    end

    -- Default: don't propagate keys to game/bindings
    -- SetPropagateKeyboardInput is protected - cannot call during combat
    if not InCombatLockdown() then
        self.editBox:SetPropagateKeyboardInput(false)
    end

    -- For Enter with visible dropdown: apply completion BEFORE SecureAction check
    if key == "ENTER" and self.suggestionsDropdown and self.suggestionsDropdown:IsVisible() then
        local selected = self.suggestionsDropdown:GetSelected()
        if selected and not self._previewActive then
            -- Apply the selected completion first
            self:InsertCompletion(selected.text)
            -- Arm SecureAction with the completed text
            if C3.SecureAction then
                C3.SecureAction:CheckAndActivate(self.editBox:GetText(), self)
            end
        end
        self:_clearPreviewState()
        self.suggestionsDropdown:Hide()
    end

    -- Handle Enter key for secure actions
    if key == "ENTER" then
        if C3.SecureAction and C3.SecureAction:IsActive() then
            -- CRITICAL: Let the binding system see this Enter key
            -- SetPropagateKeyboardInput is protected - cannot call during combat
            if not InCombatLockdown() then
                self.editBox:SetPropagateKeyboardInput(true)
            end

            if C3.SecureAction:HandleEnterKeyDown(self.editBox) then
                -- SecureAction handled it - cleared text, cleared focus
                -- The key release will trigger the override binding

                -- Clean up UI state that SecureAction doesn't know about
                self:_clearPreviewState()
                if self.suggestionsDropdown then
                    self.suggestionsDropdown:Hide()
                end
                return
            end
        end
    end

    -- Process through middleware
    if self:_processMiddleware(key) then
        return
    end
end

function InputController:_onTabPressed()
    local text = self.editBox:GetText()
    local cursorPos = self.editBox:GetCursorPosition()

    -- Get text up to cursor
    local beforeCursor = text:sub(1, cursorPos)

    -- Find the word being completed
    local word = beforeCursor:match("([%w_%-%.]+)$") or ""

    if word == "" then
        return
    end

    -- Try to complete
    local completions = self:_getCompletions(word, beforeCursor)

    if #completions == 0 then
        -- No completions
        return
    elseif #completions == 1 then
        -- Single completion - apply it
        local completion = completions[1].text
        local afterCursor = text:sub(cursorPos + 1)
        local newBefore = beforeCursor:sub(1, -#word - 1) .. completion
        self.editBox:SetText(newBefore .. afterCursor)
        self.editBox:SetCursorPosition(#newBefore)
    else
        -- Multiple completions - show in output or find common prefix
        local common = self:_findCommonPrefix(completions)
        if #common > #word then
            local afterCursor = text:sub(cursorPos + 1)
            local newBefore = beforeCursor:sub(1, -#word - 1) .. common
            self.editBox:SetText(newBefore .. afterCursor)
            self.editBox:SetCursorPosition(#newBefore)
        end

        -- Fire callback to show completions
        if self.onShowCompletions then
            self.onShowCompletions(completions)
        end
    end
end

function InputController:_onTextChanged()
    -- Block all input in read-only mode (catches stray characters from prompts)
    if self._readOnly then
        self.editBox:SetText("")
        return
    end

    -- Pre-held key characters are filtered in _onChar before they can persist

    -- Suppress the next character (used by prompt system to discard Y/N keypress)
    if self._suppressNextChar then
        self._suppressNextChar = false
        self.editBox:SetText("")
        return
    end

    -- Skip if we're programmatically updating the text (e.g., during preview)
    if self._suppressTextChanged then
        return
    end

    self.currentInput = self.editBox:GetText()
    local cursorPos = self.editBox:GetCursorPosition()

    -- Reset history navigation when typing
    if self.isNavigatingHistory then
        self.isNavigatingHistory = false
    end

    -- Handle preview mode: if user types during preview, accept it and continue
    if self._previewActive then
        -- The text changed while in preview - user typed something
        -- Accept the preview (the new text already includes their keystroke)
        self:_clearPreviewState()

        -- Hide dropdown since they're continuing to type
        if self.suggestionsDropdown then
            self.suggestionsDropdown:Hide()
        end
    end

    -- Check and activate secure action binding when typing action commands
    -- This allows single-Enter execution: binding is ready BEFORE Enter is pressed
    if C3.SecureAction then
        local success = C3.SecureAction:CheckAndActivate(self.currentInput, self)

        -- Update visual indicator based on result
        if success == false then
            -- Combat prevented binding update - warn user with red prompt
            self:_setCombatBlockedIndicator(true)
        else
            -- Normal state or no action command
            self:_setCombatBlockedIndicator(false)
        end
    end

    -- Store intellisense context for use by InsertCompletion
    if C3.Intellisense then
        self._lastIntellisenseContext = C3.Intellisense:GetContext(self.currentInput, cursorPos)
    end

    -- Fire suggestions callback for intellisense (debounced to reduce overhead)
    if self.onSuggestionsRequested then
        if self._suggestionTimer then
            self._suggestionTimer:Cancel()
        end
        local input, pos = self.currentInput, cursorPos
        self._suggestionTimer = C_Timer.NewTimer(0.03, function()
            self._suggestionTimer = nil
            self.onSuggestionsRequested(input, pos)
        end)
    end

    -- Fire callback
    if self.onTextChanged then
        self.onTextChanged(self.currentInput)
    end
end

function InputController:_onFocusGained()
    -- Track pre-held keys by watching for characters that arrive without OnKeyDown
    -- This allows new keypresses to work while blocking pre-held key repeat
    self._filteringPreHeldChars = true
    self._preHeldChars = {}       -- Characters from keys held before focus (to filter out)
    self._freshKeyChars = {}      -- Characters from fresh keypresses (to allow)

    if self.onFocusGained then
        self.onFocusGained()
    end
end

function InputController:_onFocusLost()
    -- Clear pre-held char tracking
    self._filteringPreHeldChars = false
    self._preHeldChars = nil
    self._freshKeyChars = nil

    -- Deactivate secure action when losing focus to prevent accidental triggers
    -- (Unless we're in the middle of executing a secure action)
    if C3.SecureAction and C3.SecureAction:IsActive() and not C3.SecureAction._inClickAction then
        C3.SecureAction:Deactivate()
    end

    if self.onFocusLost then
        self.onFocusLost()
    end
end

function InputController:_onChar(char)
    if not self._filteringPreHeldChars then return end

    local charLower = char:lower()

    -- Check if this character came from a fresh keypress
    if self._freshKeyChars and self._freshKeyChars[charLower] then
        -- Fresh keypress - allow it, clear the flag for this char
        self._freshKeyChars[charLower] = nil
        return
    end

    -- This character arrived without a fresh OnKeyDown - it's from a pre-held key
    -- Add it to the filter list and immediately remove it from the editbox
    self._preHeldChars[charLower] = true

    -- Remove the character that was just inserted
    -- OnChar fires AFTER the char is inserted, so we need to undo it
    local text = self.editBox:GetText()
    local cursorPos = self.editBox:GetCursorPosition()

    -- Remove the last character (the one that was just inserted)
    if cursorPos > 0 then
        local before = text:sub(1, cursorPos - 1)
        local after = text:sub(cursorPos + 1)
        self._suppressTextChanged = true
        self.editBox:SetText(before .. after)
        self.editBox:SetCursorPosition(cursorPos - 1)
        self._suppressTextChanged = false
    end
end

function InputController:_onKeyUp(key)
    if not self._filteringPreHeldChars then return end

    -- When a key is released, check if it was a pre-held key
    local char = keyToChar(key)

    -- If this character was in our pre-held filter, remove it
    if self._preHeldChars and self._preHeldChars[char] then
        self._preHeldChars[char] = nil

        -- If no more pre-held chars, stop filtering
        if not next(self._preHeldChars) then
            self._filteringPreHeldChars = false
            self._preHeldChars = nil
            self._freshKeyChars = nil
        end
    end
end

-------------------------------------------------------------------------------
-- History Navigation
-------------------------------------------------------------------------------

function InputController:_navigateHistory(direction)
    if not self.session then return end

    if not self.isNavigatingHistory then
        -- Save current input before navigating
        self.historyBuffer = self.editBox:GetText()
        self.isNavigatingHistory = true
    end

    local entry
    if direction < 0 then
        entry = self.session:HistoryPrev()
    else
        entry = self.session:HistoryNext()
    end

    local newText
    if entry == "" and direction > 0 then
        -- Restore original input
        newText = self.historyBuffer
        self.editBox:SetText(newText)
        self.isNavigatingHistory = false
    elseif entry then
        newText = entry
        self.editBox:SetText(newText)
    end

    -- Move cursor to end
    self.editBox:SetCursorPosition(#self.editBox:GetText())

    -- Sync secure action with history text
    -- SetText() is programmatic so OnTextChanged doesn't trigger with userInput=true
    if newText and C3.SecureAction then
        C3.SecureAction:CheckAndActivate(newText, self)
    end
end

-------------------------------------------------------------------------------
-- Tab Completion
-------------------------------------------------------------------------------

function InputController:_getCompletions(word, fullText)
    local completions = {}
    word = word:lower()

    -- Determine context
    local isFirstWord = not fullText:match("%s")

    if isFirstWord then
        -- Complete commands
        local cmdCompletions = C3.Command:Autocomplete(word)
        for _, c in ipairs(cmdCompletions) do
            table.insert(completions, c)
        end
    else
        -- Context-aware completion (files, etc.)
        -- Check if looks like a path
        if word:find("/") or word:find("%.") then
            local pathCompletions = self:_getPathCompletions(word)
            for _, c in ipairs(pathCompletions) do
                table.insert(completions, c)
            end
        end
    end

    return completions
end

function InputController:_getPathCompletions(partial)
    local completions = {}
    local vfs = C3:GetVFS()

    -- Determine directory and prefix
    local dir, prefix
    if partial:match("/$") then
        dir = partial
        prefix = ""
    else
        dir = C3.Paths.Parent(partial)
        prefix = C3.Paths.Filename(partial):lower()
    end

    -- List directory
    local entries = vfs:List(dir)
    if entries then
        for _, entry in ipairs(entries) do
            if entry.name:lower():sub(1, #prefix) == prefix then
                local path = C3.Paths.Join(dir, entry.name)
                if entry.type == "dir" then
                    path = path .. "/"
                end
                table.insert(completions, {
                    text = path,
                    description = entry.type,
                })
            end
        end
    end

    return completions
end

function InputController:_findCommonPrefix(completions)
    if #completions == 0 then return "" end
    if #completions == 1 then return completions[1].text end

    local prefix = completions[1].text
    for i = 2, #completions do
        local text = completions[i].text
        local j = 1
        while j <= #prefix and j <= #text and prefix:sub(j, j) == text:sub(j, j) do
            j = j + 1
        end
        prefix = prefix:sub(1, j - 1)
    end

    return prefix
end

-------------------------------------------------------------------------------
-- Public API
-------------------------------------------------------------------------------

---Focus the input box
function InputController:Focus()
    self.editBox:SetFocus()
end

---Clear focus
function InputController:ClearFocus()
    self.editBox:ClearFocus()
end

---Check if focused
---@return boolean
function InputController:HasFocus()
    return self.editBox:HasFocus()
end

---Get current text
---@return string
function InputController:GetText()
    return self.editBox:GetText()
end

---Set text
---@param text string
function InputController:SetText(text)
    self.editBox:SetText(text)
    self.editBox:SetCursorPosition(#text)
end

---Set prompt text
---@param prompt string
function InputController:SetPrompt(prompt)
    self.prompt:SetText(prompt)
    -- Adjust text insets
    local width = self.prompt:GetStringWidth() + 4
    self.editBox:SetTextInsets(width, 4, 0, 0)
end

---Set session (called when switching tabs)
---@param session table
function InputController:SetSession(session)
    self.session = session
end

---Enable/disable input
---@param enabled boolean
function InputController:SetEnabled(enabled)
    if enabled then
        self.editBox:Enable()
    else
        self.editBox:Disable()
    end
end

---Update font size (called when zoom changes)
function InputController:UpdateFontSize()
    local Theme = C3.UI.Theme
    if not Theme then return end

    local fontSize = Theme:GetEffectiveFontSize()
    local inputHeight = Theme:GetEffectiveInputHeight()
    local font = Theme.Font or "Fonts\\FRIZQT__.TTF"

    -- Update EditBox height and font
    self.editBox:SetHeight(inputHeight)
    self.editBox:SetFont(font, fontSize, "")

    -- Update prompt font
    if self.prompt then
        self.prompt:SetFont(font, fontSize, "")
        -- Recalculate text insets for new font width
        C_Timer.After(0, function()
            if self.prompt and self.editBox then
                local width = self.prompt:GetStringWidth() + 8
                self.editBox:SetTextInsets(width, 4, 0, 0)
            end
        end)
    end
end

-------------------------------------------------------------------------------
-- Suggestions Dropdown Integration
-------------------------------------------------------------------------------

---Set reference to suggestions dropdown for middleware
---@param dropdown table SuggestionsDropdown instance
function InputController:SetSuggestionsDropdown(dropdown)
    self.suggestionsDropdown = dropdown

    -- Add middleware for dropdown navigation (insert at beginning to take priority)
    table.insert(self._middleware, 1, {
        name = "suggestions_nav",
        handler = function(key, ctrl, shift, alt)
            local isArrow = key == "UP" or key == "DOWN"
            local dropdownVisible = self.suggestionsDropdown and self.suggestionsDropdown:IsVisible()

            -- Alt+Arrow or no dropdown: pass through to history middleware
            if not dropdownVisible or (alt and isArrow) then return false end

            if key == "DOWN" then
                self.suggestionsDropdown:SelectNext()
                self:_showPreview()
                return true
            end

            if key == "UP" then
                self.suggestionsDropdown:SelectPrev()
                self:_showPreview()
                return true
            end

            if key == "TAB" and self.suggestionsDropdown:GetSelected() then
                self:_acceptPreview()
                return true
            end

            if key == "ESCAPE" then
                self:_cancelPreview()
                self.suggestionsDropdown:Hide()
                self._suppressEscapePressed = true
                return true
            end

            return false
        end,
    })

    -- Set callback for click selection
    dropdown:SetOnSelect(function(suggestion)
        self:InsertCompletion(suggestion.text)
        self:_clearPreviewState()
    end)
end

---Calculate prefix for completion insertion
---@param ctx table|nil Intellisense context
---@param beforeCursor string Text before cursor
---@return string prefix Text to keep before the completion
local function calculateCompletionPrefix(ctx, beforeCursor)
    -- Argument completion: keep command + space
    if ctx and not ctx.isFirstWord and ctx.command then
        local cmdEnd = beforeCursor:match("^%s*%S+()")
        if cmdEnd then
            return beforeCursor:sub(1, cmdEnd):gsub("%s+$", "") .. " "
        end
        return ""
    end

    -- Command completion: keep everything before the current word
    local wordStart = beforeCursor:match(".*()%s") or 0
    local prefix = beforeCursor:sub(1, wordStart)
    if prefix:match("%s$") then
        prefix = prefix:gsub("%s+$", " ")
    end
    return prefix
end

---Get normalized cursor text, handling WoW's || -> | conversion
---@param ctx table|nil Intellisense context
---@param editBox frame EditBox frame
---@param originalText string|nil Original text (for preview mode)
---@param originalCursor number|nil Original cursor (for preview mode)
---@return string beforeCursor
---@return string afterCursor
local function getNormalizedCursorText(ctx, editBox, originalText, originalCursor)
    if ctx then
        return ctx.beforeCursor or "", ctx.afterCursor or ""
    end

    -- Fallback: normalize manually
    local rawText = originalText or editBox:GetText()
    local rawCursor = originalCursor or editBox:GetCursorPosition()

    local text = rawText:gsub("||", "|")
    local beforeRaw = rawText:sub(1, rawCursor)
    local pipeCount = select(2, beforeRaw:gsub("||", "")) or 0
    local cursorPos = rawCursor - pipeCount

    return text:sub(1, cursorPos), text:sub(cursorPos + 1)
end

---Show preview of currently selected suggestion in the EditBox
function InputController:_showPreview()
    local selected = self.suggestionsDropdown and self.suggestionsDropdown:GetSelected()
    if not selected then return end

    -- Save original state on first preview
    if not self._previewActive then
        self._previewActive = true
        self._previewOriginalText = self.editBox:GetText()
        self._previewOriginalCursor = self.editBox:GetCursorPosition()
        self._previewContext = self._lastIntellisenseContext
    end

    local ctx = self._previewContext
    local beforeCursor, afterCursor = getNormalizedCursorText(
        ctx, self.editBox, self._previewOriginalText, self._previewOriginalCursor
    )
    local prefix = calculateCompletionPrefix(ctx, beforeCursor)

    -- Handle quoted completions: if completion is quoted and we're replacing inside quotes,
    -- skip the closing quote in afterCursor to avoid doubling
    local completion = selected.text
    if completion:match('^".*"$') and afterCursor:match('^"') then
        afterCursor = afterCursor:sub(2)
    end

    -- Build preview text (no trailing space during preview)
    local previewText = prefix .. completion .. afterCursor

    -- Suppress OnTextChanged while we update the preview
    self._suppressTextChanged = true
    self.editBox:SetText(previewText)
    self.editBox:SetCursorPosition(#prefix + #completion)
    self._suppressTextChanged = false

    -- Sync secure action with the preview text
    if C3.SecureAction then
        C3.SecureAction:CheckAndActivate(previewText, self)
    end
end

---Accept the current preview (make it permanent)
function InputController:_acceptPreview()
    if not self._previewActive then
        -- No preview, do a normal completion
        local selected = self.suggestionsDropdown and self.suggestionsDropdown:GetSelected()
        if selected then
            self:InsertCompletion(selected.text)
        end
        return
    end

    -- Add trailing space after accepting
    local text = self.editBox:GetText()
    local cursorPos = self.editBox:GetCursorPosition()
    local afterCursor = text:sub(cursorPos + 1)

    self._suppressTextChanged = true
    if afterCursor == "" or not afterCursor:match("^[%s|]") then
        self.editBox:SetText(text:sub(1, cursorPos) .. " " .. afterCursor)
        self.editBox:SetCursorPosition(cursorPos + 1)
    end
    self._suppressTextChanged = false

    -- Update context for the new text
    local newText = self.editBox:GetText()
    local newCursor = self.editBox:GetCursorPosition()
    self._lastIntellisenseContext = C3.Intellisense:GetContext(newText, newCursor)

    self:_clearPreviewState()

    -- Request new suggestions for the completed text (e.g., field values after field name)
    -- Cancel any pending debounced request for immediate response
    if self._suggestionTimer then
        self._suggestionTimer:Cancel()
        self._suggestionTimer = nil
    end
    if self.onSuggestionsRequested then
        self.onSuggestionsRequested(newText, newCursor)
    end
end

---Cancel the preview and revert to original text
function InputController:_cancelPreview()
    if not self._previewActive then return end

    self._suppressTextChanged = true
    self.editBox:SetText(self._previewOriginalText)
    self.editBox:SetCursorPosition(self._previewOriginalCursor)
    self._suppressTextChanged = false

    self._lastIntellisenseContext = self._previewContext

    self:_clearPreviewState()
end

---Clear preview state
function InputController:_clearPreviewState()
    self._previewActive = false
    self._previewOriginalText = ""
    self._previewOriginalCursor = 0
    self._previewContext = nil
end

---Insert a completion at the current word position
---@param completion string The text to insert
function InputController:InsertCompletion(completion)
    local ctx = self._lastIntellisenseContext
    local beforeCursor, afterCursor = getNormalizedCursorText(ctx, self.editBox)
    local prefix = calculateCompletionPrefix(ctx, beforeCursor)

    -- Handle quoted completions: if completion is quoted and we're replacing inside quotes,
    -- skip the closing quote in afterCursor to avoid doubling
    if completion:match('^".*"$') and afterCursor:match('^"') then
        afterCursor = afterCursor:sub(2)
    end

    -- Append space if there's no text after or it doesn't start with space/pipe
    local suffix = (afterCursor == "" or not afterCursor:match("^[%s|]")) and " " or ""

    -- Build and set new text
    local newText = prefix .. completion .. suffix .. afterCursor
    local newCursor = #prefix + #completion + #suffix
    self.editBox:SetText(newText)
    self.editBox:SetCursorPosition(newCursor)

    -- Update context for the new text
    self._lastIntellisenseContext = C3.Intellisense:GetContext(newText, newCursor)

    -- Hide dropdown before requesting new suggestions
    if self.suggestionsDropdown then
        self.suggestionsDropdown:Hide()
    end

    -- Request new suggestions for the completed text (e.g., field values after field name)
    -- Cancel any pending debounced request for immediate response
    if self._suggestionTimer then
        self._suggestionTimer:Cancel()
        self._suggestionTimer = nil
    end
    if self.onSuggestionsRequested then
        self.onSuggestionsRequested(newText, newCursor)
    end
end

return InputController
