--[[
    Commander 3.0 - Terminal Window

    Lean orchestrator (~200 lines) that coordinates:
    - OutputDisplay (buffer visualization)
    - InputController (command input)
    - Session management
    - Command execution

    This is NOT a God Class - it delegates to components.
]]

local ADDON_NAME, C3 = ...

-------------------------------------------------------------------------------
-- Terminal Window Class
-------------------------------------------------------------------------------

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

-- Window defaults
local DEFAULT_WIDTH = 700
local DEFAULT_HEIGHT = 450
local MIN_WIDTH = 400
local MIN_HEIGHT = 250

-- External tab bar (above window when multiple tabs)
local TAB_BAR_HEIGHT = 28

-- Screen buffer defaults
local BUFFER_COLS = 80
local BUFFER_ROWS = 24

local FILESYSTEM_COMMANDS = {
    cat = true,
    cd = true,
    cp = true,
    df = true,
    du = true,
    find = true,
    fsck = true,
    ls = true,
    mkdir = true,
    mv = true,
    pwd = true,
    rm = true,
    touch = true,
    write = true,
}

local function isFilesystemCommand(text)
    local cmd = text:match("^%s*(%S+)")
    if not cmd then return false end
    return FILESYSTEM_COMMANDS[cmd:lower()] == true
end

local function normalizeTabLabel(text)
    if not text then return nil end
    local trimmed = text:match("^%s*(.-)%s*$")
    if trimmed == "" then return nil end
    return trimmed:gsub("%s+", " ")
end

-- Border colors for different states
local BORDER_COLORS = {
    default = { 0.25, 0.25, 0.25, 1 },
    combat = { 0.6, 0.15, 0.15, 1 },   -- Red tint
    focused = { 0.45, 0.55, 0.62, 0.5 }, -- Subtle blue-gray
}

---Create a new terminal window
---@param options? table { id?, session?, width?, height?, skipInitialTab? }
function TerminalWindow:init(options)
    options = options or {}

    self.id = options.id or self:_generateId()
    self._skipInitialTab = options.skipInitialTab

    -- Session will be set when tab is added/switched
    -- Don't create session here - let Tab manage session lifecycle
    self.session = nil

    -- Create main frame
    self:_createFrame(options)

    -- Create components (pass options for skipInitialTab check)
    self:_createComponents(options)

    -- Link session to terminal for command access (e.g., 'clear')
    if self.session then
        self.session.terminal = self
    end

    -- Wire up events
    self:_wireEvents()

    -- Register with window registry
    if C3.WindowRegistry then
        C3.WindowRegistry:Register(self)
    end

    -- Initial state (only show welcome if we have a session)
    if not options.skipInitialTab then
        self:_showWelcome()
    end

    C3:Debug("Terminal: Created window " .. self.id)
end

function TerminalWindow:_generateId()
    return string.format("term_%x", GetTime() * 1000)
end

-------------------------------------------------------------------------------
-- Frame Creation
-------------------------------------------------------------------------------

function TerminalWindow:_createFrame(options)
    local frameName = "Commander3Terminal_" .. self.id

    -- Inherit size: explicit options > focused window > saved size > defaults
    local width = options.width
    local height = options.height
    if not width or not height then
        local focused = C3.WindowRegistry and C3.WindowRegistry:GetFocused()
        if focused and focused.frame then
            width = width or focused.frame:GetWidth()
            height = height or focused.frame:GetHeight()
        else
            -- No focused window - use last saved size (persisted across sessions)
            local saved = C3.SessionManager:GetLastWindowSize()
            if saved then
                width = width or saved.width
                height = height or saved.height
            end
        end
    end

    self.frame = CreateFrame("Frame", frameName, UIParent, "BackdropTemplate")
    self.frame:SetSize(width or DEFAULT_WIDTH, height or DEFAULT_HEIGHT)

    -- Restore position: explicit options > saved position > default center
    local savedPos = C3.SessionManager:GetLastWindowPosition()
    if savedPos then
        self.frame:SetPoint(savedPos.point, UIParent, savedPos.relativePoint, savedPos.x, savedPos.y)
    else
        self.frame:SetPoint("CENTER")
    end
    self.frame:SetMovable(true)
    self.frame:SetResizable(true)
    self.frame:SetClampedToScreen(true)
    self.frame:SetResizeBounds(MIN_WIDTH, MIN_HEIGHT)
    self.frame:EnableMouse(true)
    self.frame:SetFrameStrata("DIALOG")  -- Above cast bars and proc effects

    -- Modern minimal backdrop (flat, thin border) - uses theme colors
    self.frame:SetBackdrop({
        bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    local winBg = C3.UI.Theme.colors.window.bg
    local winBorder = C3.UI.Theme.colors.window.border
    self.frame:SetBackdropColor(winBg[1], winBg[2], winBg[3], winBg[4])
    self.frame:SetBackdropBorderColor(winBorder[1], winBorder[2], winBorder[3], winBorder[4])

    -- External tab bar (sits ABOVE main window, shown when multiple tabs)
    self:_createTabBar(winBorder)

    -- Window controls [e][□][x]
    self:_createWindowControls()

    -- Content area (for output + inline input)
    -- Initial position assumes single tab (hidden tabs), will be updated by OnTabCountChanged
    self.content = CreateFrame("Frame", nil, self.frame)
    self.content:SetPoint("TOPLEFT", 4, -4)
    self.content:SetPoint("BOTTOMRIGHT", -4, -4)
    self.content:EnableMouse(true)
    self.content:RegisterForDrag("LeftButton")
    self.content:SetScript("OnMouseDown", function(_, button)
        if button == "LeftButton" then
            -- Bring window to front via registry
            if C3.WindowRegistry then
                C3.WindowRegistry:Focus(self)
            end
            -- Focus input
            if self.input then
                self.input:Focus()
            end
        end
    end)
    self.content:SetScript("OnDragStart", function()
        self:_startWindowDrag()
    end)
    self.content:SetScript("OnDragStop", function()
        self:_stopWindowDrag()
    end)

    -- Resize handle
    self:_createResizeHandle()

    -- Make draggable by title bar
    self.frame:SetScript("OnDragStart", function() self:_startWindowDrag() end)
    self.frame:SetScript("OnDragStop", function() self:_stopWindowDrag() end)

    -- Click anywhere on frame to focus input (helps when clicking back after losing focus)
    self.frame:SetScript("OnMouseDown", function(_, button)
        if button == "LeftButton" and self.input then
            self.input:Focus()
        end
    end)

    -- Start hidden
    self.frame:Hide()
end

function TerminalWindow:_createTabBar(winBorder)
    -- External tab bar frame - sits ABOVE the main window
    self.tabBar = CreateFrame("Frame", nil, self.frame, "BackdropTemplate")
    self.tabBar:SetPoint("BOTTOMLEFT", self.frame, "TOPLEFT", 0, -1)  -- 1px overlap for seamless look
    self.tabBar:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", 0, -1)
    self.tabBar:SetHeight(TAB_BAR_HEIGHT)
    self.tabBar:EnableMouse(true)
    self.tabBar:RegisterForDrag("LeftButton")

    -- Tab bar backdrop (darker, seamless with window)
    local tabBarColors = C3.UI.Theme.colors.tabBar or C3.UI.Theme.colors.window
    self.tabBar:SetBackdrop({
        bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 0 },  -- No bottom inset for seamless join
    })
    local tabBarBg = tabBarColors.bg or { 0.06, 0.07, 0.09, 0.88 }
    self.tabBar:SetBackdropColor(tabBarBg[1], tabBarBg[2], tabBarBg[3], tabBarBg[4] or 0.88)
    self.tabBar:SetBackdropBorderColor(winBorder[1], winBorder[2], winBorder[3], winBorder[4])

    -- Tab bar drag moves the main window
    local self_ = self
    self.tabBar:SetScript("OnDragStart", function()
        self_:_startWindowDrag()
    end)
    self.tabBar:SetScript("OnDragStop", function()
        self_:_stopWindowDrag()
    end)

    -- Click to focus window and input
    self.tabBar:SetScript("OnMouseDown", function(_, button)
        if button == "LeftButton" then
            if C3.WindowRegistry then
                C3.WindowRegistry:Focus(self_)
            end
            if self_.input then
                self_.input:Focus()
            end
        end
    end)

    -- Re-layout tabs when tabBar resizes (during window resize)
    self.tabBar:SetScript("OnSizeChanged", function()
        if self.tabs then
            self.tabs:_layoutTabs()
        end
    end)

    -- Initially hidden (single tab mode)
    self.tabBar:Hide()

    -- Create border overlay that sits on top of everything
    self:_createBorderOverlay()
end

function TerminalWindow:_createBorderOverlay()
    -- Border overlay frame - sits on top of both main frame and tabBar
    -- Only contains border edges, no fill
    local overlay = CreateFrame("Frame", nil, self.frame)
    overlay:SetFrameStrata("DIALOG")
    overlay:SetFrameLevel(self.frame:GetFrameLevel() + 50)  -- Above tabBar

    -- Create border textures (1px lines on each edge)
    local borderSize = 1
    overlay.borders = {}

    -- Top border
    overlay.borders.top = overlay:CreateTexture(nil, "OVERLAY")
    overlay.borders.top:SetHeight(borderSize)
    overlay.borders.top:SetPoint("TOPLEFT", 0, 0)
    overlay.borders.top:SetPoint("TOPRIGHT", 0, 0)

    -- Bottom border
    overlay.borders.bottom = overlay:CreateTexture(nil, "OVERLAY")
    overlay.borders.bottom:SetHeight(borderSize)
    overlay.borders.bottom:SetPoint("BOTTOMLEFT", 0, 0)
    overlay.borders.bottom:SetPoint("BOTTOMRIGHT", 0, 0)

    -- Left border
    overlay.borders.left = overlay:CreateTexture(nil, "OVERLAY")
    overlay.borders.left:SetWidth(borderSize)
    overlay.borders.left:SetPoint("TOPLEFT", 0, 0)
    overlay.borders.left:SetPoint("BOTTOMLEFT", 0, 0)

    -- Right border
    overlay.borders.right = overlay:CreateTexture(nil, "OVERLAY")
    overlay.borders.right:SetWidth(borderSize)
    overlay.borders.right:SetPoint("TOPRIGHT", 0, 0)
    overlay.borders.right:SetPoint("BOTTOMRIGHT", 0, 0)

    -- Initial color (default)
    local color = BORDER_COLORS.default
    for _, tex in pairs(overlay.borders) do
        tex:SetColorTexture(color[1], color[2], color[3], color[4])
    end

    self.borderOverlay = overlay
    self:_updateBorderOverlayBounds()
end

function TerminalWindow:_updateBorderOverlayBounds()
    if not self.borderOverlay then return end

    -- Position overlay to cover both main frame and tabBar when visible
    self.borderOverlay:ClearAllPoints()

    if self.tabBar and self.tabBar:IsShown() then
        -- Cover both tabBar and main frame
        self.borderOverlay:SetPoint("TOPLEFT", self.tabBar, "TOPLEFT", 0, 0)
        self.borderOverlay:SetPoint("BOTTOMRIGHT", self.frame, "BOTTOMRIGHT", 0, 0)
    else
        -- Just cover main frame
        self.borderOverlay:SetPoint("TOPLEFT", self.frame, "TOPLEFT", 0, 0)
        self.borderOverlay:SetPoint("BOTTOMRIGHT", self.frame, "BOTTOMRIGHT", 0, 0)
    end
end

---Lock or unlock window dragging (used during tab drags)
---@param locked boolean
function TerminalWindow:SetDragLocked(locked)
    self._dragLocked = locked and true or false
end

---@return boolean
function TerminalWindow:IsDragLocked()
    return self._dragLocked == true
end

function TerminalWindow:BeginTabTransfer()
    self._suppressEmptyClose = true
end

function TerminalWindow:EndTabTransfer()
    self._suppressEmptyClose = false
end

function TerminalWindow:_startWindowDrag()
    if self:IsDragLocked() then return end
    if C3.DragController and not C3.DragController:IsDraggingTab() then
        C3.DragController:BeginWindowDrag(self)
    end
    self.frame:StartMoving()
end

function TerminalWindow:_stopWindowDrag()
    self.frame:StopMovingOrSizing()
    if C3.DragController and C3.DragController:IsDraggingWindow() then
        C3.DragController:EndDrag()
    end
    -- Save window position for persistence
    local point, _, relativePoint, x, y = self.frame:GetPoint(1)
    if point then
        C3.SessionManager:SetLastWindowPosition(point, relativePoint, x, y)
    end
end

function TerminalWindow:_createWindowControls()
    -- Container for [e][□][x] buttons - floats in top-right, matches tab button height
    local CTRL_SIZE = 18  -- Match TAB_HEIGHT - 2 (same as tab buttons)
    local CTRL_SPACING = 2

    self.controls = CreateFrame("Frame", nil, self.frame)
    self.controls:SetPoint("TOPRIGHT", -4, -5)  -- -5 to vertically align with tabs (which have 1px top margin)
    self.controls:SetSize(CTRL_SIZE * 3 + CTRL_SPACING * 2, CTRL_SIZE)
    self.controls:SetFrameLevel(self.frame:GetFrameLevel() + 10)
    self.controls:EnableMouse(true)

    -- Create square buttons with backgrounds matching tab style (󱉫 󰊓 from left to right)
    self.closeBtn = self:_createControlButton(self.controls, "", 0, CTRL_SIZE, function() self:Close() end, {1, 0.3, 0.3})
    self.maximizeBtn = self:_createControlButton(self.controls, "󰊓", -(CTRL_SIZE + CTRL_SPACING), CTRL_SIZE, function() self:ToggleMaximize() end, {0.3, 0.6, 1})
    self.exportBtn = self:_createControlButton(self.controls, "󱉫", -(CTRL_SIZE + CTRL_SPACING) * 2, CTRL_SIZE, function() self:ShowExportPopup() end, {1, 1, 1})

    -- Store pre-maximize size for restore
    self._preMaxSize = nil

    -- Fade on hover when tabs are hidden (single tab)
    local self_ = self
    self.controls:SetScript("OnEnter", function()
        if self_.tabs and #self_.tabs.tabs <= 1 then
            self_.controls:SetAlpha(1)
        end
    end)
    self.controls:SetScript("OnLeave", function()
        if self_.tabs and #self_.tabs.tabs <= 1 then
            self_.controls:SetAlpha(0.2)
        end
    end)

    -- Create invisible drag area for when tabs are hidden
    self.dragArea = CreateFrame("Frame", nil, self.frame)
    self.dragArea:SetPoint("TOPLEFT", 4, -4)
    self.dragArea:SetPoint("TOPRIGHT", self.controls, "TOPLEFT", -4, 0)
    self.dragArea:SetHeight(20)
    self.dragArea:EnableMouse(true)
    self.dragArea:RegisterForDrag("LeftButton")
    self.dragArea:SetScript("OnDragStart", function() self:_startWindowDrag() end)
    self.dragArea:SetScript("OnDragStop", function() self:_stopWindowDrag() end)
    self.dragArea:SetScript("OnMouseDown", function(_, button)
        if button == "LeftButton" and self.input then
            self.input:Focus()
        end
    end)
end

function TerminalWindow:_createControlButton(parent, label, xOffset, size, onClick, hoverColor)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetSize(size, size)
    btn:SetPoint("RIGHT", xOffset, 0)
    btn:EnableMouse(true)

    -- Background (using theme button colors)
    local bg = btn:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    local btnNormal = C3.UI.Theme and C3.UI.Theme:GetColor("button.normal") or { 0.12, 0.14, 0.17, 0.75 }
    local btnHover = C3.UI.Theme and C3.UI.Theme:GetColor("button.hover") or { 0.18, 0.20, 0.25, 0.90 }
    local btnTextNormal = C3.UI.Theme and C3.UI.Theme:GetColor("button.textNormal") or { 0.56, 0.74, 0.73, 1 }
    bg:SetColorTexture(btnNormal[1], btnNormal[2], btnNormal[3], btnNormal[4])
    btn._bg = bg

    local text = btn:CreateFontString(nil, "OVERLAY")
    local font = C3.UI.Theme and C3.UI.Theme.Font or "Fonts\\FRIZQT__.TTF"
    text:SetFont(font, 10, "")
    text:SetPoint("CENTER")
    text:SetText(label)
    text:SetTextColor(btnTextNormal[1], btnTextNormal[2], btnTextNormal[3], btnTextNormal[4])
    btn._text = text

    -- Default hover color is white
    local hc = hoverColor or {1, 1, 1}

    btn:SetScript("OnClick", onClick)
    btn:SetScript("OnEnter", function()
        bg:SetColorTexture(btnHover[1], btnHover[2], btnHover[3], btnHover[4])
        text:SetTextColor(hc[1], hc[2], hc[3])
        -- Also trigger parent hover for alpha fade
        if parent:GetScript("OnEnter") then
            parent:GetScript("OnEnter")(parent)
        end
    end)
    btn:SetScript("OnLeave", function()
        bg:SetColorTexture(btnNormal[1], btnNormal[2], btnNormal[3], btnNormal[4])
        text:SetTextColor(btnTextNormal[1], btnTextNormal[2], btnTextNormal[3], btnTextNormal[4])
        if parent:GetScript("OnLeave") then
            parent:GetScript("OnLeave")(parent)
        end
    end)

    return btn
end

function TerminalWindow:ToggleMaximize()
    local PADDING = 20  -- Padding around edges when maximized

    if self._preMaxSize then
        -- Restore
        self.frame:ClearAllPoints()
        self.frame:SetPoint(unpack(self._preMaxSize.point))
        self.frame:SetSize(self._preMaxSize.width, self._preMaxSize.height)
        self._preMaxSize = nil
        self.maximizeBtn._text:SetText("󰊓")
    else
        -- Store current state
        local point, relativeTo, relativePoint, x, y = self.frame:GetPoint(1)
        self._preMaxSize = {
            point = { point, relativeTo, relativePoint, x, y },
            width = self.frame:GetWidth(),
            height = self.frame:GetHeight(),
        }
        -- Account for external tab bar when visible
        local tabBarOffset = (self.tabBar and self.tabBar:IsShown()) and TAB_BAR_HEIGHT or 0
        -- Maximize with padding (extra space for tab bar above)
        self.frame:ClearAllPoints()
        self.frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", PADDING, -PADDING - tabBarOffset)
        self.frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -PADDING, PADDING)
        self.maximizeBtn._text:SetText("󰊔")  -- Fullscreen exit icon
    end
    self:_onResize()
end

---Called by TabStrip when tab count changes
---@param count number
function TerminalWindow:OnTabCountChanged(count)
    local showTabs = count > 1

    if showTabs then
        -- Show external tab bar (tabs are now inside tabBar, not main frame)
        self.tabBar:Show()
        self.tabs.frame:Show()

        -- Reparent controls to tab bar
        self.controls:SetParent(self.tabBar)
        self.controls:ClearAllPoints()
        self.controls:SetPoint("RIGHT", self.tabBar, "RIGHT", -4, 0)
        self.controls:SetFrameLevel(self.tabBar:GetFrameLevel() + 10)
        self.controls:SetAlpha(1)

        -- Content stays at window top (tabs are external now)
        self.content:SetPoint("TOPLEFT", 4, -4)

        self.dragArea:Hide()
    else
        -- Hide external tab bar
        self.tabBar:Hide()
        self.tabs.frame:Hide()

        -- Reparent controls back to main frame
        self.controls:SetParent(self.frame)
        self.controls:ClearAllPoints()
        self.controls:SetPoint("TOPRIGHT", self.frame, "TOPRIGHT", -4, -5)
        self.controls:SetFrameLevel(self.frame:GetFrameLevel() + 10)
        self.controls:SetAlpha(0.2)

        -- Content at full height
        self.content:SetPoint("TOPLEFT", 4, -4)

        self.dragArea:Show()
    end

    -- Update border overlay to cover tabBar when visible
    self:_updateBorderOverlayBounds()
end

function TerminalWindow:_createResizeHandle()
    self.resizeHandle = CreateFrame("Button", nil, self.frame)
    self.resizeHandle:SetPoint("BOTTOMRIGHT", -2, 2)
    self.resizeHandle:SetSize(16, 16)
    self.resizeHandle:EnableMouse(true)
    self.resizeHandle:SetFrameLevel(self.frame:GetFrameLevel() + 20)  -- Ensure on top

    local tex = self.resizeHandle:CreateTexture(nil, "OVERLAY")
    tex:SetAllPoints()
    tex:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")

    self.resizeHandle:SetScript("OnMouseDown", function()
        -- Focus window when starting resize
        if C3.WindowRegistry then
            C3.WindowRegistry:Focus(self)
        end
        self.frame:StartSizing("BOTTOMRIGHT")
    end)
    self.resizeHandle:SetScript("OnMouseUp", function()
        self.frame:StopMovingOrSizing()
        -- Clear maximize state when user manually resizes
        if self._preMaxSize then
            self._preMaxSize = nil
            if self.maximizeBtn then
                self.maximizeBtn._text:SetText("󰊓")
            end
        end
        self:_onResize()
    end)
end

function TerminalWindow:UpdateBorderColor()
    local color
    local showOverlay = false
    if self.inCombat then
        color = BORDER_COLORS.combat
        showOverlay = true
    elseif self.hasFocus then
        color = BORDER_COLORS.focused
        showOverlay = true
    else
        color = BORDER_COLORS.default
        showOverlay = false
    end

    -- Border overlay only shown when focused/combat (to appear above tabs)
    -- Hidden otherwise to avoid z-order issues with other windows
    if self.borderOverlay then
        if showOverlay then
            self.borderOverlay:SetFrameLevel(self.frame:GetFrameLevel() + 50)
            if self.borderOverlay.borders then
                for _, tex in pairs(self.borderOverlay.borders) do
                    tex:SetColorTexture(color[1], color[2], color[3], color[4])
                end
            end
            self.borderOverlay:Show()
        else
            self.borderOverlay:Hide()
        end
    end

    -- Always update backdrop borders
    self.frame:SetBackdropBorderColor(unpack(color))
    if self.tabBar then
        self.tabBar:SetBackdropBorderColor(unpack(color))
    end
end

-------------------------------------------------------------------------------
-- Component Creation
-------------------------------------------------------------------------------

function TerminalWindow:_createComponents()
    -- Output display (uses VirtualListRenderer)
    self.output = C3.UI.OutputDisplay:new(self.content, {
        buffer = C3.VirtualBuffer:new({ maxLines = 5000 }),
        scrollbarAnchor = self.frame,  -- Anchor scrollbar to main frame for consistent position
    })

    -- Wire output to terminal for context menu input manipulation
    self.output:SetTerminal(self)

    -- Wire up click-to-focus and drag events on output area
    if self.output.renderer then
        self.output.renderer.onFocusRequested = function()
            -- Bring window to front
            if C3.WindowRegistry then
                C3.WindowRegistry:Focus(self)
            end
            -- Focus input
            if self.input then
                self.input:Focus()
            end
        end
        -- Wire up drag events to use DragController (enables merge detection from anywhere)
        self.output.renderer.onDragStart = function()
            self:_startWindowDrag()
        end
        self.output.renderer.onDragStop = function()
            self:_stopWindowDrag()
        end
    end

    -- Track current mode (NORMAL or TUI)
    self.mode = "NORMAL"

    -- Input controller (positioned inline at bottom of content area)
    self.input = C3.UI.InputController:new(self.frame, {
        session = self.session,
        terminal = self,  -- Reference for CastProgress
    })
    -- Position input at bottom of content (inline with output)
    self.input.editBox:ClearAllPoints()
    self.input.editBox:SetPoint("BOTTOMLEFT", self.content, "BOTTOMLEFT", 4, 10)
    self.input.editBox:SetPoint("BOTTOMRIGHT", self.content, "BOTTOMRIGHT", -4, 10)

    -- Suggestions dropdown (for intellisense)
    if C3.UI.SuggestionsDropdown then
        self.suggestions = C3.UI.SuggestionsDropdown:new(self.frame)
        self.input:SetSuggestionsDropdown(self.suggestions)

        -- Wire up suggestions request callback
        self.input.onSuggestionsRequested = function(text, cursorPos)
            if C3.Intellisense then
                local suggestions = C3.Intellisense:GetSuggestions(text, cursorPos)
                -- Get the partial query for match highlighting
                local ctx = C3.Intellisense:GetContext(text, cursorPos)
                local partial = ctx and ctx.partial or ""
                self.suggestions:Show(suggestions, self.input.editBox, partial)
            end
        end
    end

    -- Tab strip (optional - for multi-session support)
    if C3.UI.TabStrip then
        self.tabs = C3.UI.TabStrip:new(self.frame, {
            window = self,  -- Parent window reference for cross-window operations
            onTabSelected = function(tab) self:SwitchToTab(tab) end,
            onTabClosed = function(tab) self:_onTabClosed(tab) end,
            onTabCreated = function(tab) self:SwitchToTab(tab) end
        })
        -- Create initial tab (unless skipInitialTab)
        -- Tab will create and own its own session
        if not self._skipInitialTab then
            self.tabs:CreateTab({ name = "Terminal" })
        end
    end
end

-------------------------------------------------------------------------------
-- Event Wiring
-------------------------------------------------------------------------------

function TerminalWindow:_wireEvents()
    -- Input controller callbacks
    self.input.onCommand = function(text)
        self:ExecuteCommand(text)
    end

    self.input.onCancel = function()
        self:Hide()
    end

    self.input.onClear = function()
        self.output:Clear()
    end

    self.input.onPageUp = function()
        self.output:PageUp()
    end

    self.input.onPageDown = function()
        self.output:PageDown()
    end

    self.input.onShowCompletions = function(completions)
        self:_showCompletions(completions)
    end

    -- Frame events
    self.frame:SetScript("OnShow", function()
        self.input:Focus()
    end)

    -- Border state tracking
    self.inCombat = UnitAffectingCombat("player")
    self.hasFocus = false

    -- Combat events for border color
    self.frame:RegisterEvent("PLAYER_REGEN_DISABLED")
    self.frame:RegisterEvent("PLAYER_REGEN_ENABLED")
    self.frame:SetScript("OnEvent", function(_, event)
        if event == "PLAYER_REGEN_DISABLED" then
            self.inCombat = true
        elseif event == "PLAYER_REGEN_ENABLED" then
            self.inCombat = false
        end
        self:UpdateBorderColor()
    end)

    -- Focus tracking: Window focus is managed by WindowRegistry:Focus().
    -- When editbox loses focus, check if focus went to another Commander3 window.
    -- If not, clear this window's focus state (clicked outside all windows).
    local self_ = self
    self.input.onFocusGained = function()
        -- When editbox gains focus, ensure this window is marked as focused
        if C3.WindowRegistry then
            C3.WindowRegistry:Focus(self_)
        else
            self_.hasFocus = true
            self_:UpdateBorderColor()
        end
    end

    self.input.onFocusLost = function()
        -- Small delay to allow WindowRegistry:Focus() to run first if clicking another window
        C_Timer.After(0.01, function()
            -- If this window is still marked as focused but editbox lost focus,
            -- and no other window took focus, then we clicked outside
            if self_.hasFocus then
                local currentFocused = C3.WindowRegistry and C3.WindowRegistry:GetFocused()
                if currentFocused == self_ then
                    -- Check if mouse is still over this window (e.g., dragging resize handle)
                    if self_.frame:IsMouseOver() then
                        return
                    end
                    -- We're still the "focused" window but lost editbox focus = clicked outside
                    self_.hasFocus = false
                    C3.WindowRegistry._focusedWindow = nil
                    self_:UpdateBorderColor()
                end
            end
        end)
    end

    self:UpdateBorderColor()

    -- Register for zoom changes
    if C3.UI.Theme then
        C3.UI.Theme:OnZoomChanged(function(scale, size)
            self:_onZoomChanged(scale, size)
        end)
    end
end

-------------------------------------------------------------------------------
-- Zoom Handling
-------------------------------------------------------------------------------

---Handle zoom change - update all components
---@param scale number The new zoom scale (e.g., 1.0 for 100%)
---@param size number The effective font size (e.g., 12 for 100% zoom)
function TerminalWindow:_onZoomChanged(scale, size)
    -- Update main components
    if self.output then
        self.output:UpdateFontSize()
    end

    if self.input then
        self.input:UpdateFontSize()
    end

    if self.suggestions then
        self.suggestions:UpdateFontSize()
    end

    -- Update status bar
    if self.statusBar then
        self.statusBar:UpdateFontSize()
    end

    -- Update all tabs in TabStrip
    if self.tabs and self.tabs.tabs then
        for _, tab in ipairs(self.tabs.tabs) do
            if tab.UpdateFontSize then
                tab:UpdateFontSize()
            end
        end
    end

    -- Update context menu if initialized
    if self.output and self.output.contextMenu then
        self.output.contextMenu:UpdateFontSize()
    end

end

-------------------------------------------------------------------------------
-- Command Execution
-------------------------------------------------------------------------------

function TerminalWindow:ExecuteCommand(text)
    local rawText = text
    local expandedText = self.session:ExpandAliases(text)

    self:_updateActiveTabNameFromCommand(expandedText)

    if isFilesystemCommand(expandedText) then
        if self.session and self.session.EnableCwdPrompt then
            self.session:EnableCwdPrompt()
        end
        if self.input and (not C3.Prompt or not C3.Prompt:IsActive()) then
            self.input:UpdatePrompt()
        end
    end

    -- Handle exit command silently (no echo, just close)
    if expandedText:lower():match("^exit%s*$") then
        self:Hide()
        return
    end

    -- Immediate commands that must run in hardware event context (e.g., ReloadUI)
    -- These fail with taint errors if executed from the async kernel's timer context
    local IMMEDIATE_COMMANDS = {
        reload = function() ReloadUI() end,
        rl = function() ReloadUI() end,
    }

    local cmdName = expandedText:match("^%s*(%S+)")
    if cmdName then
        local handler = IMMEDIATE_COMMANDS[cmdName:lower()]
        if handler then
            local promptText = self.input:GetPromptString()
            self.output:AddInput(rawText, promptText)
            handler()
            return
        end
    end

    -- Echo input with class-colored prompt
    local promptText = self.input:GetPromptString()
    self.output:AddInput(rawText, promptText)

    -- Set active buffer for this execution (allows commands to output to correct window)
    if self.session then
        self.session._activeBuffer = self.output.buffer
    end

    -- Execute
    local task = C3:Execute(expandedText, self.session)

    -- Handle result
    task:OnComplete(function(_, result)
        -- Clear buffer reference after complete
        if self.session then
            self.session._activeBuffer = nil
        end
        if result then
            self:_handleResult(result)
        end
        if self.input and (not C3.Prompt or not C3.Prompt:IsActive()) then
            self.input:UpdatePrompt()
        end
    end)

    task:OnError(function(_, err)
        -- Clear buffer reference after error
        if self.session then
            self.session._activeBuffer = nil
        end
        self.output:AddError(tostring(err))
        if self.input and (not C3.Prompt or not C3.Prompt:IsActive()) then
            self.input:UpdatePrompt()
        end
    end)
end

function TerminalWindow:_updateActiveTabNameFromCommand(text)
    local label = normalizeTabLabel(text)
    if not label then return end

    if self.tabs and self.tabs.GetActiveTab then
        local activeTab = self.tabs:GetActiveTab()
        if activeTab and activeTab.SetLabel then
            activeTab:SetLabel(label)
        end
    end
end

function TerminalWindow:_handleResult(result)
    if result == nil then
        return
    end

    -- FIX: Handle function/iterator results explicitly with yielding
    if type(result) == "function" then
        local collected = C3.Pipeline.Collect(result)
        local self_ = self
        C3._kernel:Run(function()
            local YIELD_INTERVAL = 100  -- Higher with deferred rendering
            for i, item in ipairs(collected) do
                self_:_handleResult(item)
                if i % YIELD_INTERVAL == 0 then
                    coroutine.yield()
                end
            end
            -- Release pooled objects after consumption
            C3.Types.ReleaseAll(collected)
        end)
        return
    end

    if type(result) == "table" then
        if result._type == "Error" then
            self.output:AddError(result.message or "Unknown error")
        elseif result._type == "PromptRequest" then
            -- Y/N confirmation prompt - delegate to Prompt system
            C3.Prompt:Show(result, self)
        elseif result._type == "PromptQueueRequest" then
            -- Bulk operation queue - delegate to Prompt queue system
            C3.Prompt:QueueSecure(result, self)
        elseif result._type == "PromptQueue" then
            -- Queue system handles its own output, nothing to do here
            return
        elseif result._type == "Component" then
            -- Rich component output (icon grid, progress bar, etc.)
            self.output:ShowComponent(result.componentType, result.data)
        elseif result._type then
            -- Single typed object
            local formatted = C3.Formatter:Format(result)
            -- Split by newlines to handle multi-line text (e.g., help output)
            for line in formatted:gmatch("[^\n]+") do
                self.output:AddLine(C3.BufferLine.Output(line))
            end
        elseif result[1] ~= nil then
            -- Array of results - schedule as task to allow yielding
            local items = result
            local self_ = self
            C3._kernel:Run(function()
                local YIELD_INTERVAL = 100  -- Can be higher now with deferred rendering
                for i, item in ipairs(items) do
                    self_:_handleResult(item)
                    if i % YIELD_INTERVAL == 0 then
                        coroutine.yield()
                    end
                end
                -- Release pooled objects after consumption
                C3.Types.ReleaseAll(items)
            end)
        else
            -- Iterator or generic table
            if type(result) == "function" or (getmetatable(result) and getmetatable(result).__call) then
                -- Iterator - collect and display with yielding
                local collected = C3.Pipeline.Collect(result)
                local self_ = self
                C3._kernel:Run(function()
                    local YIELD_INTERVAL = 100  -- Higher with deferred rendering
                    for i, item in ipairs(collected) do
                        local formatted = C3.Formatter:Format(item)
                        self_.output:AddLine(C3.BufferLine.Output(formatted))
                        if i % YIELD_INTERVAL == 0 then
                            coroutine.yield()
                        end
                    end
                    -- Release pooled objects after consumption
                    C3.Types.ReleaseAll(collected)
                end)
            else
                -- Generic table
                local formatted = C3.Formatter:Format(result)
                self.output:AddLine(C3.BufferLine.Output(formatted))
            end
        end
    else
        self.output:AddLine(C3.BufferLine.Text(tostring(result)))
    end
end

-------------------------------------------------------------------------------
-- Helpers
-------------------------------------------------------------------------------

function TerminalWindow:_showWelcome()
    self.output:AddLine(C3.BufferLine.Text(
        C3.Formatter:Colorize("Commander 3.0", "00FFFF") .. " - " ..
        C3.Formatter:Colorize("Programmable Data Environment", "888888")))
    self.output:AddLine(C3.BufferLine.Text(
        C3.Formatter:Colorize("Type 'help' for commands, 'test' to verify system.", "666666")))
    self.output:AddLine(C3.BufferLine.Separator())
end

function TerminalWindow:_showCompletions(completions)
    if #completions <= 10 then
        local texts = {}
        for _, c in ipairs(completions) do
            table.insert(texts, c.text)
        end
        self.output:AddLine(C3.BufferLine.Text(table.concat(texts, "  ")))
    else
        self.output:AddLine(C3.BufferLine.Text(
            string.format("(%d completions)", #completions)))
    end
end

function TerminalWindow:ShowExportPopup()
    -- Fallback to old popup if Editor not available
    if not self.exportPopup then
        self:_createExportPopup()
    end

    -- Build debug header
    local debugLines = {
        "=== COMMANDER 3.0 DEBUG EXPORT ===",
        "Version: " .. (C3.VERSION or "unknown"),
        "Flavor: " .. (C3.Version and C3.Version:GetFlavor() or "unknown"),
        "TOC: " .. (C3.Version and C3.Version._toc or "unknown"),
        "Time: " .. date("%Y-%m-%d %H:%M:%S"),
        "",
        "=== SYSTEM STATUS ===",
        "Kernel: " .. (C3._kernel and "OK" or "MISSING"),
        "Parser: " .. (C3.Parser and "OK" or "MISSING"),
        "Pipeline: " .. (C3.Pipeline and "OK" or "MISSING"),
        "Types: " .. (C3.Types and "OK" or "MISSING"),
        "Context: " .. (C3.Context and "OK" or "MISSING"),
        "",
        "=== REGISTERED COMMANDS ===",
    }

    -- List registered commands
    local cmdList = {}
    if C3.Commands then
        for name, _ in pairs(C3.Commands) do
            table.insert(cmdList, name)
        end
        table.sort(cmdList)
    end
    table.insert(debugLines, table.concat(cmdList, ", "))
    table.insert(debugLines, "")
    table.insert(debugLines, "=== TERMINAL OUTPUT ===")
    table.insert(debugLines, "")

    -- Get buffer text
    local bufferText = self.output:GetBuffer():ExportText()

    -- Combine
    local text = table.concat(debugLines, "\n") .. bufferText

    -- Set text and show
    self.exportPopup.editBox:SetText(text)
    self.exportPopup:Show()
    self.exportPopup.editBox:SetFocus()
    self.exportPopup.editBox:HighlightText()
end

function TerminalWindow:_createExportPopup()
    local popup = CreateFrame("Frame", "Commander3ExportPopup", UIParent, "BackdropTemplate")
    popup:SetSize(500, 400)
    popup:SetPoint("CENTER")
    popup:SetMovable(true)
    popup:EnableMouse(true)
    popup:SetClampedToScreen(true)
    popup:SetFrameStrata("DIALOG")
    popup:RegisterForDrag("LeftButton")
    popup:SetScript("OnDragStart", function(f) f:StartMoving() end)
    popup:SetScript("OnDragStop", function(f) f:StopMovingOrSizing() end)

    popup:SetBackdrop({
        bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 16,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    local popupBg = C3.UI.Theme and C3.UI.Theme:GetColor("content.bg") or { 0.06, 0.07, 0.09, 0.85 }
    local popupBorder = C3.UI.Theme and C3.UI.Theme:GetColor("window.border") or { 0.22, 0.25, 0.31, 0.85 }
    popup:SetBackdropColor(popupBg[1], popupBg[2], popupBg[3], 0.95)
    popup:SetBackdropBorderColor(popupBorder[1], popupBorder[2], popupBorder[3], popupBorder[4])

    -- Theme font
    local themeFont = C3.UI.Theme and C3.UI.Theme.Font or "Fonts\\FRIZQT__.TTF"
    local themeFontSize = C3.UI.Theme and C3.UI.Theme.FontSize or 12

    -- Title
    local title = popup:CreateFontString(nil, "OVERLAY")
    title:SetFont(themeFont, themeFontSize, "")
    title:SetPoint("TOPLEFT", 10, -10)
    title:SetText("Export Output - Press Ctrl+A then Ctrl+C to copy")
    title:SetTextColor(1, 0.82, 0)

    -- Close button
    local closeBtn = CreateFrame("Button", nil, popup, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", 0, 0)
    closeBtn:SetScript("OnClick", function() popup:Hide() end)

    -- Scroll frame for editbox
    local scrollFrame = CreateFrame("ScrollFrame", nil, popup, "UIPanelScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", 10, -35)
    scrollFrame:SetPoint("BOTTOMRIGHT", -30, 40)

    -- Edit box (multi-line)
    local editBox = CreateFrame("EditBox", nil, scrollFrame)
    editBox:SetMultiLine(true)
    editBox:SetAutoFocus(false)
    editBox:SetFont(themeFont, themeFontSize - 1, "")
    editBox:SetWidth(scrollFrame:GetWidth() - 10)
    editBox:SetTextColor(0.9, 0.9, 0.9)
    editBox:SetScript("OnEscapePressed", function() popup:Hide() end)

    scrollFrame:SetScrollChild(editBox)
    popup.editBox = editBox

    -- Close button at bottom
    local closeButton = CreateFrame("Button", nil, popup, "UIPanelButtonTemplate")
    closeButton:SetSize(80, 22)
    closeButton:SetPoint("BOTTOM", 0, 10)
    closeButton:SetText("Close")
    closeButton:SetScript("OnClick", function() popup:Hide() end)

    popup:Hide()
    self.exportPopup = popup
end

function TerminalWindow:_onResize()
    -- Save last window size and position to session manager for persistence
    C3.SessionManager:SetLastWindowSize(self.frame:GetWidth(), self.frame:GetHeight())
    local point, _, relativePoint, x, y = self.frame:GetPoint(1)
    if point then
        C3.SessionManager:SetLastWindowPosition(point, relativePoint, x, y)
    end

    -- Update viewport size based on new height
    local height = self.content:GetHeight()
    local lineHeight = 14 -- Approximate
    local viewportSize = math.floor(height / lineHeight)
    self.output:GetBuffer():SetViewportSize(viewportSize)
    self.output:Refresh()

    -- Re-layout tabs to fit new width
    if self.tabs then
        self.tabs:_layoutTabs()
    end
end

-------------------------------------------------------------------------------
-- Tab Management
-------------------------------------------------------------------------------

---Switch to a tab (updates session, wires buffer, updates UI)
---@param tab Tab
function TerminalWindow:SwitchToTab(tab)
    if not tab then return end

    -- Update session reference
    self.session = tab.session
    self.session.terminal = self  -- Maintain terminal reference
    C3.SessionManager:SetActive(self.session)

    -- Update input controller's session for history navigation
    if self.input and self.input.SetSession then
        self.input:SetSession(self.session)
    end

    -- Handle TUI mode when switching tabs
    -- Check if the NEW session has an active TUI app
    local newSessionHasTui = tab.session and tab.session._tuiApp

    if newSessionHasTui then
        -- New session has TUI app - enter/restore TUI mode for this session
        self:_restoreTuiMode(tab.session._tuiApp)
    elseif self._tuiMode then
        -- Current terminal is in TUI mode but new session doesn't have TUI
        -- Hide TUI overlay without stopping the app (it belongs to another session)
        self:_suspendTuiMode()
    end

    -- Wire tab's buffer to output display (uses Tab's encapsulated method)
    if tab.WireBufferTo then
        tab:WireBufferTo(self.output)
    elseif tab.buffer then
        -- Fallback for legacy tab objects
        self.output.buffer = tab.buffer
        if self.output.renderer then
            self.output.renderer:SetBuffer(tab.buffer)
        end
    end

    -- Refresh output after tab switch (only if not in TUI mode)
    if not self._tuiMode then
        self.output:Refresh()
    end

    -- Focus appropriate input
    if self._tuiMode and self._tuiInputFrame then
        self._tuiInputFrame:SetFocus()
    else
        self.input:Focus()
    end

    -- Update input for the active session
    self.input.session = self.session
    self.input:UpdatePrompt()

    -- Update UI (suppress callback to prevent recursion loop)
    if self.tabs then
        self.tabs:SelectTab(tab, true)
    end
end

---Adopt a tab from another window (clean transfer)
---@param tab Tab
---@param position number? Insert position
function TerminalWindow:AdoptTab(tab, position)
    if not self.tabs then return end

    -- Add tab to our strip
    self.tabs:AddTab(tab, position)

    -- Switch to the adopted tab
    self:SwitchToTab(tab)

    -- Focus this window
    if C3.WindowRegistry then
        C3.WindowRegistry:Focus(self)
    end
end

---Close a tab
---@param tab Tab
function TerminalWindow:CloseTab(tab)
    if not self.tabs then return end

    self.tabs:CloseTab(tab)
end

-------------------------------------------------------------------------------
-- TUI Mode Support
-------------------------------------------------------------------------------

---Create TUI input frame for capturing keys in TUI mode
---Uses an invisible EditBox since regular Frames can't capture keyboard focus
function TerminalWindow:_createTuiInputFrame()
    if self._tuiInputFrame then return end

    -- Use an EditBox because it can capture keyboard focus
    local frame = CreateFrame("EditBox", nil, self.content)
    frame:SetAllPoints()
    frame:SetAutoFocus(false)
    frame:EnableKeyboard(true)
    frame:SetMaxLetters(0)  -- Don't accept any text
    frame:Hide()

    -- Make it invisible but functional
    frame:SetFontObject(GameFontNormal)
    frame:SetTextColor(0, 0, 0, 0)  -- Invisible text

    local self_ = self

    -- Intercept all key presses
    frame:SetScript("OnKeyDown", function(_, key)
        if not self_._tuiMode then return end

        -- Block keys from propagating to game keybinds
        -- SetPropagateKeyboardInput is protected - cannot call during combat
        if not InCombatLockdown() then
            frame:SetPropagateKeyboardInput(false)
        end

        local ctrl = IsControlKeyDown()
        local shift = IsShiftKeyDown()
        local alt = IsAltKeyDown()

        -- Handle universal keybinds FIRST (tab management, window management)
        if ctrl and self_:_handleUniversalKeybind(key, shift, alt) then
            return
        end

        -- Build modifiers table for TUI app
        local modifiers = {
            ctrl = ctrl,
            shift = shift,
            alt = alt,
        }

        -- Route to TUI app
        self_:HandleTuiKey(key, modifiers)
    end)

    -- Prevent text input
    frame:SetScript("OnChar", function()
        frame:SetText("")
    end)

    -- Prevent escape from clearing focus unexpectedly
    frame:SetScript("OnEscapePressed", function()
        if self_._tuiMode and self_._tuiApp then
            self_:HandleTuiKey("ESCAPE", {})
        end
    end)

    -- Refocus if focus is lost while in TUI mode
    frame:SetScript("OnEditFocusLost", function()
        if self_._tuiMode then
            C_Timer.After(0, function()
                if self_._tuiMode and frame:IsShown() then
                    frame:SetFocus()
                end
            end)
        end
    end)

    self._tuiInputFrame = frame
end

---Handle universal keybinds that work even in TUI mode
---@param key string
---@param shift boolean
---@param alt boolean
---@return boolean handled
function TerminalWindow:_handleUniversalKeybind(key, shift, alt)
    local tabs = self.tabs

    -- Ctrl+T: New tab
    if key == "T" and not shift and not alt then
        if tabs then
            tabs:CreateTab({ activate = true })
        end
        return true
    end

    -- Ctrl+W: Close current tab (or window if only one tab)
    if key == "W" and not shift and not alt then
        if tabs then
            if tabs:GetTabCount() > 1 then
                -- Stop TUI app in current session before closing
                if self._tuiApp and self._tuiApp.Stop then
                    self._tuiApp:Stop()
                end
                local activeTab = tabs:GetActiveTab()
                if activeTab then
                    tabs:CloseTab(activeTab)
                end
            else
                -- Only one tab - close the window
                if self._tuiApp and self._tuiApp.Stop then
                    self._tuiApp:Stop()
                end
                self:Close()
            end
        end
        return true
    end

    -- Ctrl+Tab / Ctrl+Shift+Tab: Next/Previous tab
    if key == "TAB" and not alt then
        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
    end

    -- Ctrl+1-9: Switch to tab by number
    local num = tonumber(key)
    if num and num >= 1 and num <= 9 and not shift and not alt then
        if tabs then
            local allTabs = tabs:GetTabs()
            if allTabs[num] then
                tabs:SelectTab(allTabs[num])
            end
        end
        return true
    end

    -- Ctrl+N: New window
    if key == "N" and not shift and not alt then
        C3:NewWindowAtCursor()
        return true
    end

    return false
end

---Shared helper: Activate TUI mode (show TUI overlay, hide normal input)
---@param app table TUI App instance
function TerminalWindow:_activateTuiMode(app)
    self._tuiApp = app
    self._tuiMode = true

    -- Create TUI renderer if needed
    if not self._tuiRenderer then
        self._tuiRenderer = C3.TUI.Renderer:new(self)
    end

    -- Create TUI input frame if needed
    self:_createTuiInputFrame()

    -- Show TUI overlay
    self._tuiRenderer:Show()

    -- Disable normal input handling
    if self.input then
        self.input.editBox:Hide()
        self.input._tuiMode = true
    end

    -- Hide suggestions
    if self.suggestions then
        self.suggestions:Hide()
    end

    -- Show and focus TUI input frame
    self._tuiInputFrame:Show()
    self._tuiInputFrame:SetFocus()

    self.mode = "TUI"
end

---Shared helper: Deactivate TUI mode (hide TUI overlay, restore normal input)
---@param focusInput boolean Whether to focus the normal input after deactivation
function TerminalWindow:_deactivateTuiMode(focusInput)
    self._tuiApp = nil
    self._tuiMode = false

    -- Hide TUI input frame
    if self._tuiInputFrame then
        self._tuiInputFrame:Hide()
    end

    -- Hide TUI overlay
    if self._tuiRenderer then
        self._tuiRenderer:Hide()
    end

    -- Re-enable normal input
    if self.input then
        self.input.editBox:Show()
        self.input._tuiMode = false
        if focusInput then
            self.input:Focus()
        end
    end

    self.mode = "NORMAL"
end

---Enter TUI mode (app takes over rendering)
---@param app table TUI App instance
function TerminalWindow:EnterTuiMode(app)
    if not app then return end

    -- Store app in session for tab isolation
    if self.session then
        self.session._tuiApp = app
    end

    self:_activateTuiMode(app)
    C3:Debug("Terminal: Entered TUI mode")
end

---Exit TUI mode (return to normal terminal)
function TerminalWindow:ExitTuiMode()
    -- Clear app from session
    if self.session then
        self.session._tuiApp = nil
    end

    self:_deactivateTuiMode(true)
    C3:Debug("Terminal: Exited TUI mode")
end

---Suspend TUI mode (hide overlay without stopping app - for tab switching)
function TerminalWindow:_suspendTuiMode()
    self:_deactivateTuiMode(false)
end

---Restore TUI mode for an app (when switching to a tab with active TUI)
---@param app table TUI App instance
function TerminalWindow:_restoreTuiMode(app)
    if not app then return end

    -- CRITICAL: Update app's terminal reference to this window
    -- This is needed when a tab with a TUI app is transferred to a new window
    if app.SetTerminal then
        app:SetTerminal(self)
    else
        app.terminal = self  -- Direct assignment fallback
    end

    self:_activateTuiMode(app)

    -- Force a render of the app
    if app.ForceRender then
        app:ForceRender()
    elseif app.Render then
        app:Render()
    end
end

---Check if in TUI mode
---@return boolean
function TerminalWindow:IsInTuiMode()
    return self._tuiMode == true
end

---Get the active TUI app
---@return table|nil
function TerminalWindow:GetTuiApp()
    return self._tuiApp
end

---Render a TUI buffer to the display
---@param buffer table TuiBuffer
function TerminalWindow:RenderTuiBuffer(buffer)
    if self._tuiRenderer then
        self._tuiRenderer:Render(buffer)
    end
end

---Get TUI dimensions in characters
---@return number cols, number rows
function TerminalWindow:GetTuiSize()
    if self._tuiRenderer then
        return self._tuiRenderer:GetBufferSize()
    end
    return BUFFER_COLS, BUFFER_ROWS
end

---Route key to TUI app
---@param key string
---@param modifiers? table { ctrl, shift, alt }
---@return boolean handled
function TerminalWindow:HandleTuiKey(key, modifiers)
    if not self._tuiApp then return false end

    -- Get action from input handler
    local action = C3.TUI.Input.GetAction(key, modifiers)

    if action then
        -- Action-based handling
        local handled = self._tuiApp:HandleInput(action, key)
        return handled
    else
        -- Raw key handling (for games)
        self._tuiApp:HandleRawKey(key, true)
        return true
    end
end

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

function TerminalWindow:Show()
    -- If tabs were cleaned up (window was closed), create a fresh tab
    if self.tabs and self.tabs:GetTabCount() == 0 then
        self.tabs:CreateTab({ name = "Terminal" })
    end
    self.frame:Show()
    -- Clear keybind flag (no longer need to suppress - EditBox isn't focused during keybind)
    C3._toggledViaKeybind = nil
    -- Focus appropriate input based on mode
    if self._tuiMode and self._tuiInputFrame then
        self._tuiInputFrame:SetFocus()
    else
        self.input:Focus()
    end
    -- Resume live updates
    if self.output then
        self.output:SetVisible(true)
    end
end

function TerminalWindow:Hide()
    self.frame:Hide()
    -- Clear keybind flag (in case hide was triggered via keybind)
    C3._toggledViaKeybind = nil
    -- Pause live updates
    if self.output then
        self.output:SetVisible(false)
    end
end

function TerminalWindow:Toggle()
    if self.frame:IsShown() then
        self:Hide()
    else
        self:Show()
    end
end

function TerminalWindow:Clear()
    self.output:Clear()
end

function TerminalWindow:Print(text)
    self.output:AddLine(C3.BufferLine.Text(text))
end

function TerminalWindow:GetSession()
    return self.session
end

---Close this window (primary window hides instead)
function TerminalWindow:Close()
    -- Primary windows just hide - preserve tabs and sessions so they work when reopened
    if self._isPrimary then
        if C3.WindowRegistry then
            C3.WindowRegistry:Close(self)
        else
            self:Hide()
        end
        return
    end

    -- Non-primary windows: clean up tabs and their sessions before destroying
    self._suppressEmptyClose = true
    if self.tabs then
        local tabsToClose = {}
        for _, tab in ipairs(self.tabs:GetTabs()) do
            tabsToClose[#tabsToClose + 1] = tab
        end
        for _, tab in ipairs(tabsToClose) do
            tab:Destroy()
        end
    end
    self.session = nil
    self._suppressEmptyClose = false

    if C3.WindowRegistry then
        C3.WindowRegistry:Close(self)
    else
        self:Hide()
    end
end

---Handle tab closed event - close window if no tabs remain
---@param tab table
function TerminalWindow:_onTabClosed(tab)
    if self._suppressEmptyClose then
        return
    end

    -- If this was the last tab and we're not primary, close the window
    if self.tabs and self.tabs:GetTabCount() == 0 then
        if not self._isPrimary then
            self:Close()
        else
            -- Primary window: create a new default tab
            self.tabs:CreateTab({ name = "Terminal" })
        end
    end
end

-------------------------------------------------------------------------------
-- Primary Window Access (replaces singleton pattern)
-------------------------------------------------------------------------------

function C3:GetTerminal()
    -- Use registry to get primary window, or create if none exists
    if C3.WindowRegistry then
        local primary = C3.WindowRegistry:GetPrimary()
        if primary then
            return primary
        end
    end

    -- No primary exists, create one
    local window = TerminalWindow:new()
    -- Set up Terminal shorthand for apps
    C3.Terminal = window
    return window
end

function C3:ShowTerminal()
    local terminal = self:GetTerminal()
    if C3.WindowRegistry then
        C3.WindowRegistry:Focus(terminal)
    else
        terminal:Show()
    end
end

function C3:HideTerminal()
    local terminal = C3.WindowRegistry and C3.WindowRegistry:GetPrimary()
    if terminal then
        terminal:Hide()
    end
end

function C3:ToggleTerminal()
    if C3.WindowRegistry then
        C3.WindowRegistry:ToggleAll()
    else
        self:GetTerminal():Toggle()
    end
end

---Create a new terminal window
---@param options? table
---@return table|nil window Returns nil if max windows reached
function C3:CreateTerminalWindow(options)
    -- Check limit before creating
    if C3.WindowRegistry and not C3.WindowRegistry:CanCreate() then
        return nil
    end
    return TerminalWindow:new(options)
end

---Create a new terminal window centered at cursor position
function C3:NewWindowAtCursor()
    -- Check limit first - don't proceed if at max
    if C3.WindowRegistry and not C3.WindowRegistry:CanCreate() then
        C3.WindowRegistry:ShowMaxWindowsToast()
        return nil
    end

    local scale = UIParent:GetEffectiveScale()
    local cursorX, cursorY = GetCursorPosition()
    cursorX, cursorY = cursorX / scale, cursorY / scale

    local newWindow = TerminalWindow:new()
    if newWindow and newWindow.frame then
        newWindow.frame:ClearAllPoints()
        newWindow.frame:SetPoint("CENTER", UIParent, "BOTTOMLEFT", cursorX, cursorY)
        if C3.WindowRegistry then
            C3.WindowRegistry:Focus(newWindow)
        else
            newWindow:Show()
        end
    end
    return newWindow
end

---Focus the most recently focused window, or create primary if none exists
function C3:FocusLastWindow()
    local registry = C3.WindowRegistry
    if not registry then
        self:ToggleTerminal()
        return
    end

    -- Walk backwards through window order (last = most recent)
    for i = #registry._windowOrder, 1, -1 do
        local win = registry._windows[registry._windowOrder[i]]
        if win then
            registry:Focus(win)  -- Focus() calls Show() automatically
            return
        end
    end

    -- No window exists - create/get primary
    registry:Focus(self:GetTerminal())
end

---Drop focus from Commander, returning keyboard control to WoW
function C3:DropFocus()
    -- Use WoW's global API to find current keyboard focus
    local focus = GetCurrentKeyBoardFocus()
    if focus and focus.ClearFocus then
        focus:ClearFocus()
        return
    end

    -- Fallback to registry tracking
    local registry = C3.WindowRegistry
    local focused = registry and registry:GetFocused()
    if focused and focused.input and focused.input.editBox then
        focused.input.editBox:ClearFocus()
    end
end

---Toggle focus: if Commander has focus, drop it; otherwise focus last window
function C3:ToggleFocus()
    local registry = C3.WindowRegistry
    local focused = registry and registry:GetFocused()
    if focused and focused.hasFocus then
        self:DropFocus()
    else
        self:FocusLastWindow()
    end
end

-------------------------------------------------------------------------------
-- Slash Command
-------------------------------------------------------------------------------

SLASH_COMMANDER1 = "/cmd"
SLASH_COMMANDER2 = "/commander"
SLASH_COMMANDER3 = "/c3"

SlashCmdList["COMMANDER"] = function(msg)
    if msg and msg ~= "" then
        -- Execute command directly
        C3:GetTerminal():Show()
        C3:GetTerminal():ExecuteCommand(msg)
    else
        -- Toggle window
        C3:ToggleTerminal()
    end
end

-------------------------------------------------------------------------------
-- Addon Initialization
-- This is the last file in the TOC, so we handle ADDON_LOADED here
-------------------------------------------------------------------------------

local initFrame = CreateFrame("Frame")
initFrame:RegisterEvent("ADDON_LOADED")
initFrame:SetScript("OnEvent", function(self, event, addonName)
    if addonName == ADDON_NAME then
        -- All files loaded, trigger initialization callbacks
        C3:CompleteInit()
        self:UnregisterEvent("ADDON_LOADED")
    end
end)

-------------------------------------------------------------------------------
-- Target Change Watcher (for mount match feature)
-- Refreshes suggestions when target changes while typing "mount match"
-------------------------------------------------------------------------------

local targetWatcher = CreateFrame("Frame")
targetWatcher:RegisterEvent("PLAYER_TARGET_CHANGED")
targetWatcher:SetScript("OnEvent", function()
    -- Get the active terminal (if any)
    local terminal = C3.Terminal
    if not terminal or not terminal.input then return end

    -- Check if user is typing "mount match" or similar
    local text = terminal.input.currentInput or ""
    if text:match("^mount%s+m") or text:match("^mount%s*$") then
        -- Trigger suggestion refresh
        if terminal.input.onSuggestionsRequested then
            local cursorPos = terminal.input.editBox and terminal.input.editBox:GetCursorPosition() or #text
            terminal.input.onSuggestionsRequested(text, cursorPos)
        end
    end
end)

return TerminalWindow
