local Cowmera = _G.Cowmera or {}
_G.Cowmera = Cowmera

CowmeraDB = CowmeraDB or {}
CowmeraDB.skipChat = CowmeraDB.skipChat or {}

local SKIPCHAT_DB = CowmeraDB.skipChat
local PANEL = nil
local STATUS_TEXT = nil
local KEY_CATCHER = nil
local REFRESH_TICKER = nil
local OPTION_ROWS = {}
local OVERLAYS = {}

local function EnsureSkipChatOptions()
    CowmeraDB = CowmeraDB or {}
    CowmeraDB.skipChat = CowmeraDB.skipChat or {}
    SKIPCHAT_DB = CowmeraDB.skipChat

    if SKIPCHAT_DB.enabled == nil then SKIPCHAT_DB.enabled = false end
    if SKIPCHAT_DB.useSpace == nil then SKIPCHAT_DB.useSpace = true end
    if SKIPCHAT_DB.useNumberKeys == nil then SKIPCHAT_DB.useNumberKeys = true end
    if SKIPCHAT_DB.showOptionNumbers == nil then SKIPCHAT_DB.showOptionNumbers = true end
    if SKIPCHAT_DB.enablePopupConfirm == nil then SKIPCHAT_DB.enablePopupConfirm = true end
    if SKIPCHAT_DB.disableInCombat == nil then SKIPCHAT_DB.disableInCombat = false end
    if SKIPCHAT_DB.onlyWhenDialogVisible == nil then SKIPCHAT_DB.onlyWhenDialogVisible = true end
    if SKIPCHAT_DB.blockWhenTyping == nil then SKIPCHAT_DB.blockWhenTyping = true end
    return SKIPCHAT_DB
end

local function IsEditBoxFocused()
    if ChatEdit_GetActiveWindow and ChatEdit_GetActiveWindow() then
        return true
    end

    if GetCurrentKeyBoardFocus then
        local focus = GetCurrentKeyBoardFocus()
        if focus and focus.IsObjectType and focus:IsObjectType("EditBox") then
            return true
        end
    end

    return false
end

local function ButtonHasUsefulText(button)
    if not button then return false end
    local text = nil
    if button.GetText then
        local ok, v = pcall(button.GetText, button)
        if ok then text = v end
    end
    if (not text or text == "") and button.Text and button.Text.GetText then
        local ok, v = pcall(button.Text.GetText, button.Text)
        if ok then text = v end
    end
    text = tostring(text or ""):gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "")
    text = text:gsub("^%s+", ""):gsub("%s+$", "")
    if text == "" then return false end
    return true, text
end

local function CanUseButton(button)
    if not button or not button.IsShown or not button:IsShown() then return false end
    if button.IsVisible and not button:IsVisible() then return false end
    if button.IsEnabled and not button:IsEnabled() then return false end
    if button.GetAlpha and button:GetAlpha() <= 0 then return false end
    if not button.Click and not button.GetScript then return false end
    return true
end

local function ClickButton(button)
    if not CanUseButton(button) then return false end
    if button.Click then
        local ok = pcall(button.Click, button)
        if ok then return true end
        ok = pcall(button.Click, button, "LeftButton")
        if ok then return true end
    end
    local script = button.GetScript and button:GetScript("OnClick")
    if type(script) == "function" then
        local ok = pcall(script, button, "LeftButton")
        if ok then return true end
    end
    return false
end

local function CollectVisibleButtons(root, out, seen)
    if type(root) ~= "table" or seen[root] then return end
    seen[root] = true

    if root.GetObjectType and root:GetObjectType() == "Button" and CanUseButton(root) then
        table.insert(out, root)
    end

    if root.GetChildren then
        local children = { root:GetChildren() }
        for _, child in ipairs(children) do
            CollectVisibleButtons(child, out, seen)
        end
    end
end

local function SortButtons(buttons)
    table.sort(buttons, function(a, b)
        local atop = (a.GetTop and a:GetTop()) or 0
        local btop = (b.GetTop and b:GetTop()) or 0
        if math.abs(atop - btop) > 8 then
            return atop > btop
        end

        local aleft = (a.GetLeft and a:GetLeft()) or 0
        local bleft = (b.GetLeft and b:GetLeft()) or 0
        if math.abs(aleft - bleft) > 4 then
            return aleft < bleft
        end

        local ay = select(5, a:GetPoint(1)) or 0
        local by = select(5, b:GetPoint(1)) or 0
        if math.abs(ay - by) > 8 then
            return ay > by
        end

        local ax = select(4, a:GetPoint(1)) or 0
        local bx = select(4, b:GetPoint(1)) or 0
        return ax < bx
    end)
end

local function FindStaticPopupConfirmButton()
    if not SKIPCHAT_DB.enablePopupConfirm then return nil end
    for i = 1, 4 do
        local popup = _G["StaticPopup" .. i]
        if popup and popup:IsShown() then
            local btn = _G[popup:GetName() .. "Button1"]
            if CanUseButton(btn) then
                return btn
            end
        end
    end
    return nil
end

local function FindQuestActionButton()
    local candidates = {
        _G.QuestFrameCompleteQuestButton,
        _G.QuestFrameCompleteButton,
        _G.QuestFrameAcceptButton,
        _G.QuestFrameFinishButton,
        _G.QuestFrameGoodbyeButton,
        _G.ItemTextNextPageButton,
        _G.ItemTextPageText and _G.ItemTextFrame and _G.ItemTextFrame.NextButton or nil,
        _G.GossipFrameGreetingPanel and _G.GossipFrameGreetingPanel.GoodbyeButton or nil,
        _G.GossipFrame and _G.GossipFrame.CloseButton or nil,
    }

    for _, btn in ipairs(candidates) do
        if CanUseButton(btn) then
            return btn
        end
    end

    return nil
end

local function HasSupportedDialogVisible()
    if FindStaticPopupConfirmButton() then return true end
    local frames = {
        _G.GossipFrame,
        _G.QuestFrame,
        _G.ItemTextFrame,
    }
    for _, frame in ipairs(frames) do
        if frame and frame.IsShown and frame:IsShown() then
            return true
        end
    end
    return false
end

local function GatherOptionButtons()
    local roots = {}
    local buttons = {}
    local seen = {}

    if _G.GossipFrame and _G.GossipFrame:IsShown() then
        table.insert(roots, _G.GossipFrame)
    end
    if _G.QuestFrame and _G.QuestFrame:IsShown() then
        table.insert(roots, _G.QuestFrame)
    end
    if _G.ItemTextFrame and _G.ItemTextFrame:IsShown() then
        table.insert(roots, _G.ItemTextFrame)
    end

    for _, root in ipairs(roots) do
        CollectVisibleButtons(root, buttons, seen)
    end

    local filtered = {}
    local dedupe = {}
    for _, button in ipairs(buttons) do
        local ok, text = ButtonHasUsefulText(button)
        if ok then
            local lower = string.lower(text)
            local skip = false
            if lower:find("accept") or lower:find("complete") or lower:find("continue") or lower:find("goodbye") or lower:find("cancel") or lower:find("decline") or lower:find("good bye") or lower:find("close") then
                skip = true
            end
            if not skip and not dedupe[button] then
                dedupe[button] = true
                table.insert(filtered, button)
            end
        end
    end

    SortButtons(filtered)
    return filtered
end

local function GetPrimaryActionButton()
    local popupButton = FindStaticPopupConfirmButton()
    if popupButton then return popupButton end

    local questButton = FindQuestActionButton()
    if questButton then return questButton end

    local options = GatherOptionButtons()
    return options[1]
end

local function ShouldHandleKeys()
    local opts = EnsureSkipChatOptions()
    if not opts.enabled then return false, "Disabled" end
    if opts.disableInCombat and InCombatLockdown and InCombatLockdown() then
        return false, "Disabled in combat"
    end
    if opts.blockWhenTyping and IsEditBoxFocused() then
        return false, "Typing in chat or edit box" end
    if opts.onlyWhenDialogVisible and not HasSupportedDialogVisible() then
        return false, "No supported dialog visible"
    end
    return true, "Ready"
end

local function PressPrimaryAction()
    if not SKIPCHAT_DB.useSpace then return false end
    local button = GetPrimaryActionButton()
    if not button then return false end
    return ClickButton(button)
end

local function PressOptionIndex(index)
    if not SKIPCHAT_DB.useNumberKeys then return false end
    local options = GatherOptionButtons()
    local button = options[index]
    if not button then return false end
    return ClickButton(button)
end

local function HideOverlays()
    for _, overlay in ipairs(OVERLAYS) do
        overlay:Hide()
        overlay.button = nil
    end
end

local function EnsureOverlay(index)
    if OVERLAYS[index] then return OVERLAYS[index] end
    local holder = CreateFrame("Frame", nil, UIParent)
    holder:SetSize(18, 18)
    holder:SetFrameStrata("DIALOG")
    holder:SetFrameLevel(9999)
    holder:Hide()

    local text = holder:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    text:SetPoint("CENTER", holder, "CENTER", 0, 0)
    text:SetTextColor(1, 0.82, 0)
    text:SetFont(GameFontNormal:GetFont(), 13, "OUTLINE")
    text:SetText(tostring(index))
    holder.text = text
    OVERLAYS[index] = holder
    return holder
end

local function UpdateOptionOverlays()
    local opts = EnsureSkipChatOptions()
    if not opts.enabled or not opts.useNumberKeys or not opts.showOptionNumbers then
        HideOverlays()
        return
    end
    local shouldHandle = ShouldHandleKeys()
    if not shouldHandle then
        HideOverlays()
        return
    end

    local options = GatherOptionButtons()
    local shown = 0
    for index = 1, math.min(#options, 9) do
        local button = options[index]
        if CanUseButton(button) then
            local overlay = EnsureOverlay(index)
            overlay:ClearAllPoints()
            overlay:SetParent(UIParent)
            overlay:SetPoint("RIGHT", button, "LEFT", -4, 0)
            overlay.text:SetText(tostring(index))
            overlay.button = button
            overlay:Show()
            shown = index
        end
    end

    for index = shown + 1, #OVERLAYS do
        if OVERLAYS[index] then
            OVERLAYS[index]:Hide()
            OVERLAYS[index].button = nil
        end
    end
end



local PENDING_PROPAGATE = nil
local PROPAGATE_EVENT_FRAME = CreateFrame("Frame")
PROPAGATE_EVENT_FRAME:RegisterEvent("PLAYER_REGEN_ENABLED")
PROPAGATE_EVENT_FRAME:SetScript("OnEvent", function(_, event)
    if event == "PLAYER_REGEN_ENABLED" and PENDING_PROPAGATE ~= nil and KEY_CATCHER and KEY_CATCHER.SetPropagateKeyboardInput then
        local pending = PENDING_PROPAGATE and true or false
        PENDING_PROPAGATE = nil
        pcall(KEY_CATCHER.SetPropagateKeyboardInput, KEY_CATCHER, pending)
    end
end)

local function SetKeyCatcherPropagation(enabled)
    if not KEY_CATCHER or not KEY_CATCHER.SetPropagateKeyboardInput then
        return
    end
    enabled = enabled and true or false
    if InCombatLockdown and InCombatLockdown() then
        PENDING_PROPAGATE = enabled
        return
    end
    pcall(KEY_CATCHER.SetPropagateKeyboardInput, KEY_CATCHER, enabled)
    PENDING_PROPAGATE = nil
end

local function UpdateKeyCatcherState()
    local opts = EnsureSkipChatOptions()
    if not KEY_CATCHER then return end

    local shouldCapture = false
    if opts.enabled then
        local canHandle = ShouldHandleKeys()
        if canHandle then
            shouldCapture = true
        elseif opts.onlyWhenDialogVisible == false then
            -- Keep the old behavior only when the user explicitly allows hotkeys
            -- outside supported dialog windows.
            shouldCapture = true
        end
    end

    if shouldCapture then
        KEY_CATCHER:Show()
        KEY_CATCHER:EnableKeyboard(true)
    else
        SetKeyCatcherPropagation(true)
        KEY_CATCHER:EnableKeyboard(false)
        KEY_CATCHER:Hide()
    end
end

local function UpdateStatusText()
    if not STATUS_TEXT then return end
    local ok, reason = ShouldHandleKeys()
    if ok then
        STATUS_TEXT:SetText("Status: |cff22c55eActive|r")
    else
        STATUS_TEXT:SetText("Status: |cffff7070" .. tostring(reason or "Inactive") .. "|r")
    end
end

function Cowmera.RefreshSkipChatPanel()
    if not PANEL then return end
    EnsureSkipChatOptions()
    for _, row in ipairs(OPTION_ROWS) do
        row.check:SetChecked(SKIPCHAT_DB[row.key] and true or false)
    end
    UpdateStatusText()
    UpdateKeyCatcherState()
    UpdateOptionOverlays()
end

local function CreateOptionRow(parent, anchor, key, label, tip)
    local check = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
    if anchor then
        check:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -8)
    else
        check:SetPoint("TOPLEFT", parent, "TOPLEFT", 20, -54)
    end
    check.Text:SetText(label)
    check.Text:SetTextColor(1, 0.82, 0, 1)
    check:SetScript("OnClick", function(self)
        EnsureSkipChatOptions()
        SKIPCHAT_DB[key] = self:GetChecked() and true or false
        CowmeraDB.skipChat = SKIPCHAT_DB
        UpdateKeyCatcherState()
        UpdateStatusText()
        UpdateOptionOverlays()
    end)
    if tip and tip ~= "" then
        check:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetText(label)
            GameTooltip:AddLine(tip, 1, 1, 1, true)
            GameTooltip:Show()
        end)
        check:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)
    end
    table.insert(OPTION_ROWS, { key = key, check = check })
    return check
end

function Cowmera.BuildSkipChatPanel(host)
    if not host or host.__cowmeraSkipChatBuilt then return end
    host.__cowmeraSkipChatBuilt = true
    PANEL = host

    EnsureSkipChatOptions()

    local title = host:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 18, -18)
    title:SetText("SkipChat")

    local subtitle = host:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    subtitle:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8)
    subtitle:SetWidth(620)
    subtitle:SetJustifyH("LEFT")
    subtitle:SetText("")

    STATUS_TEXT = host:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    STATUS_TEXT:SetPoint("TOPLEFT", subtitle, "BOTTOMLEFT", 0, -14)
    STATUS_TEXT:SetText("Status: Loading...")

    local holder = CreateFrame("Frame", nil, host, "BackdropTemplate")
    holder:SetPoint("TOPLEFT", 18, -72)
    holder:SetPoint("BOTTOMRIGHT", -18, 18)
    if holder.SetBackdrop then
        holder:SetBackdrop({
            bgFile = "Interface/ChatFrame/ChatFrameBackground",
            edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
            tile = true,
            tileSize = 16,
            edgeSize = 12,
            insets = { left = 3, right = 3, top = 3, bottom = 3 },
        })
        holder:SetBackdropColor(0, 0, 0, 0.35)
        holder:SetBackdropBorderColor(0.65, 0.4, 0.08, 0.9)
    end

    local row1 = CreateOptionRow(holder, nil, "enabled", "Enable SkipChat", "Master toggle for dialog hotkeys.")
    local row2 = CreateOptionRow(holder, row1, "useSpace", "Use Spacebar to continue / accept / complete", "Press Space to activate the primary visible quest, gossip, readable text, or popup action.")
    local row3 = CreateOptionRow(holder, row2, "useNumberKeys", "Use number keys 1-9 for options", "Press 1-9 to choose visible dialog options from top to bottom.")
    local row4 = CreateOptionRow(holder, row3, "showOptionNumbers", "Show on-screen numbers beside dialog options", "Adds a small number tag to the top-left of each visible option so you can see which hotkey matches it.")
    local row5 = CreateOptionRow(holder, row4, "enablePopupConfirm", "Enable popup confirm hotkey", "Lets Space confirm common StaticPopup dialogs.")
    local row6 = CreateOptionRow(holder, row5, "disableInCombat", "Disable in combat", "Turns SkipChat off during combat to avoid protected interaction problems.")
    local row7 = CreateOptionRow(holder, row6, "onlyWhenDialogVisible", "Only work when a supported dialog is visible", "Hotkeys do nothing unless quest, gossip, item text, or popup windows are open.")
    local row8 = CreateOptionRow(holder, row7, "blockWhenTyping", "Suspend while typing in chat or edit boxes", "Prevents Space and number keys from being stolen while typing.")

    local details = holder:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    details:SetPoint("TOPLEFT", row8, "BOTTOMLEFT", 0, -20)
    details:SetWidth(640)
    details:SetJustifyH("LEFT")
    details:SetText("")

    Cowmera.RefreshSkipChatPanel()
end

local function HandleKeyDown(_, key)
    local shouldHandle = ShouldHandleKeys()
    if not shouldHandle then
        SetKeyCatcherPropagation(true)
        UpdateStatusText()
        UpdateOptionOverlays()
        return
    end

    local handled = false
    if key == "SPACE" then
        handled = PressPrimaryAction()
    else
        local index = nil
        if key:match("^NUMPAD([1-9])$") then
            index = tonumber(key:match("^NUMPAD([1-9])$"))
        elseif key:match("^([1-9])$") then
            index = tonumber(key)
        end
        if index then
            handled = PressOptionIndex(index)
        end
    end

    SetKeyCatcherPropagation(not handled)
    UpdateStatusText()
    C_Timer.After(0, UpdateOptionOverlays)
end

local function CreateKeyCatcher()
    if KEY_CATCHER then return end
    KEY_CATCHER = CreateFrame("Frame", "CowmeraSkipChatKeyCatcher", UIParent)
    KEY_CATCHER:SetAllPoints(UIParent)
    KEY_CATCHER:EnableMouse(false)
    KEY_CATCHER:SetFrameStrata("TOOLTIP")
    SetKeyCatcherPropagation(true)
    KEY_CATCHER:SetScript("OnKeyDown", HandleKeyDown)
    KEY_CATCHER:Hide()
    UpdateKeyCatcherState()
end

local startup = CreateFrame("Frame")
startup:RegisterEvent("PLAYER_LOGIN")
startup:RegisterEvent("PLAYER_REGEN_ENABLED")
startup:RegisterEvent("PLAYER_REGEN_DISABLED")
startup:RegisterEvent("GOSSIP_SHOW")
startup:RegisterEvent("GOSSIP_CLOSED")
startup:RegisterEvent("QUEST_GREETING")
startup:RegisterEvent("QUEST_DETAIL")
startup:RegisterEvent("QUEST_PROGRESS")
startup:RegisterEvent("QUEST_COMPLETE")
startup:RegisterEvent("QUEST_FINISHED")
startup:RegisterEvent("QUEST_ACCEPTED")
startup:RegisterEvent("ITEM_TEXT_BEGIN")
startup:RegisterEvent("ITEM_TEXT_CLOSED")
startup:RegisterEvent("PLAYER_ENTERING_WORLD")
startup:RegisterEvent("PLAYER_DEAD")
startup:RegisterEvent("PLAYER_ALIVE")
startup:RegisterEvent("PLAYER_UNGHOST")
startup:SetScript("OnEvent", function(_, event)
    EnsureSkipChatOptions()
    if event == "PLAYER_LOGIN" then
        CreateKeyCatcher()
        if REFRESH_TICKER then
            REFRESH_TICKER:Cancel()
        end
        REFRESH_TICKER = C_Timer.NewTicker(0.15, function()
            UpdateStatusText()
            UpdateKeyCatcherState()
            UpdateOptionOverlays()
        end)
    else
        UpdateStatusText()
        UpdateKeyCatcherState()
        C_Timer.After(0, UpdateOptionOverlays)
    end
end)
