local addonName, RF = ...
local T

-------------------------------------------------------------------------------
-- Chat Tab: Guild chat with send, history, filter
-------------------------------------------------------------------------------

local chatLog = {}  -- { time, sender, message, type }
local MAX_MESSAGES = RF.MAX_CHAT_MESSAGES
local chatChannel = "guild"  -- "guild" or "officer"
local chatLogLoaded = false

-- Sent message history (up/down arrow recall)
local sentHistory = {}
local sentHistoryIdx = 0
local MAX_SENT_HISTORY = 50

-- Tab-complete state
local tabState = { active = false, prefix = "", matches = {}, idx = 0, replaceStart = 0, replaceEnd = 0 }

-- Emoji map: display-side raid icon replacement
local EMOJI_MAP = {
    ["{star}"]     = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_1:0|t",
    ["{circle}"]   = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_2:0|t",
    ["{diamond}"]  = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_3:0|t",
    ["{triangle}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_4:0|t",
    ["{moon}"]     = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_5:0|t",
    ["{square}"]   = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_6:0|t",
    ["{cross}"]    = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_7:0|t",
    ["{skull}"]    = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_8:0|t",
}

local function ReplaceEmojis(text)
    for pattern, icon in pairs(EMOJI_MAP) do
        text = text:gsub(pattern, icon)
    end
    return text
end

-------------------------------------------------------------------------------
-- Chat Font Helper: resolve settings and apply to RF.chatFrame
-------------------------------------------------------------------------------
function RF:ApplyChatFont()
    if not RF.chatFrame then return end
    local db = RF.charDB
    if not db then return end

    -- Resolve font path from label
    local fontPath
    for _, entry in ipairs(RF.CHAT_FONTS) do
        if entry.label == db.chatFontFace then
            fontPath = entry.path
            break
        end
    end
    if not fontPath then
        fontPath = select(1, ChatFontNormal:GetFont())
    end

    local size = db.chatFontSize or 12
    local outline = db.chatFontOutline or ""

    RF.chatFrame:SetFont(fontPath, size, outline)
end

-------------------------------------------------------------------------------
-- Persist chat log in guild SavedVariables
-------------------------------------------------------------------------------
local function GetChatDB()
    local guildDB = RF:GetGuildDB()
    if not guildDB then return nil end
    if not guildDB.chatLog then
        guildDB.chatLog = {}
    end
    return guildDB.chatLog
end

local function LoadChatLog()
    if chatLogLoaded then return end
    local saved = GetChatDB()
    if saved then
        -- Merge any messages received before guild key was available
        if chatLog ~= saved and #chatLog > 0 then
            for _, entry in ipairs(chatLog) do
                tinsert(saved, entry)
            end
            while #saved > MAX_MESSAGES do
                tremove(saved, 1)
            end
        end
        chatLog = saved
        chatLogLoaded = true
        -- Notify listeners that persisted chat is now available
        RF:FireCallbacks("CHAT_LOG_LOADED")
    end
end

local function SaveChatEntry(entry)
    local saved = GetChatDB()
    if not saved then return end
    -- chatLog and saved are the same table after LoadChatLog,
    -- but if we haven't loaded yet, push to saved too
    if saved ~= chatLog then
        tinsert(saved, entry)
        while #saved > MAX_MESSAGES do
            tremove(saved, 1)
        end
    end
end

-------------------------------------------------------------------------------
-- Keyword Match (must be above OnGuildChat which references it)
-------------------------------------------------------------------------------
local function CheckKeywordMatch(message)
    local lower = message:lower()

    -- Auto self-mention: always trigger on player's own name or @Name
    local playerName = UnitName("player")
    if playerName then
        local pLower = playerName:lower()
        if lower:find(pLower, 1, true) or lower:find("@" .. pLower, 1, true) then
            return true
        end
    end

    local keywords = RF.charDB and RF.charDB.chatKeywords
    if not keywords or #keywords == 0 then return false end
    for _, keyword in ipairs(keywords) do
        if keyword ~= "" and lower:find(keyword:lower(), 1, true) then
            return true
        end
    end
    return false
end

-------------------------------------------------------------------------------
-- Unread Badge Helper
-------------------------------------------------------------------------------
RF._chatUnreadCount = 0

function RF:UpdateChatUnreadBadge()
    if not RF.mainFrame or not RF.mainFrame.tabBar then return end
    local tab = RF.mainFrame.tabBar.tabs["Chat"]
    if not tab then return end

    -- Lazily create badge elements
    if not tab.badgeBg then
        local bg = tab:CreateTexture(nil, "OVERLAY", nil, 2)
        bg:SetSize(16, 16)
        bg:SetPoint("TOPRIGHT", tab, "TOPRIGHT", 4, -2)
        bg:SetColorTexture(0.90, 0.25, 0.25, 1.00)
        bg:SetTexCoord(0, 1, 0, 1)
        tab.badgeBg = bg

        local text = tab:CreateFontString(nil, "OVERLAY", "GameFontNormal", 3)
        local fontPath = select(1, ChatFontNormal:GetFont())
        text:SetFont(fontPath, 9, "OUTLINE")
        text:SetPoint("CENTER", bg, "CENTER", 0, 0)
        text:SetTextColor(1, 1, 1)
        tab.badgeText = text
    end

    local showBadge = RF.charDB and RF.charDB.chatShowUnreadBadge ~= false
    if showBadge and RF._chatUnreadCount > 0 then
        local display = RF._chatUnreadCount > 99 and "99+" or tostring(RF._chatUnreadCount)
        tab.badgeText:SetText(display)
        -- Resize badge for wider text
        local textW = tab.badgeText:GetStringWidth()
        tab.badgeBg:SetWidth(math.max(16, textW + 6))
        tab.badgeBg:Show()
        tab.badgeText:Show()
    else
        tab.badgeBg:Hide()
        tab.badgeText:Hide()
    end
end

-------------------------------------------------------------------------------
-- Store incoming guild messages
-------------------------------------------------------------------------------
local function OnGuildChat(event, message, sender, ...)
    LoadChatLog()

    local entry = {
        time = time(),
        sender = sender or "Unknown",
        message = message or "",
        type = (event == "CHAT_MSG_OFFICER") and "officer" or "guild",
    }
    tinsert(chatLog, entry)

    while #chatLog > MAX_MESSAGES do
        tremove(chatLog, 1)
    end

    SaveChatEntry(entry)

    -- Unread badge: increment if Chat tab is not visible
    local chatTabVisible = RF.mainFrame and RF.mainFrame:IsShown()
        and RF.mainFrame.tabBar and RF.mainFrame.tabBar.activeTab == "Chat"
    if not chatTabVisible then
        RF._chatUnreadCount = RF._chatUnreadCount + 1
        RF:UpdateChatUnreadBadge()
    end

    -- Sound alert on keyword match (throttled by chatSoundCooldown setting)
    if RF.charDB and RF.charDB.chatSoundAlerts and CheckKeywordMatch(entry.message) then
        local now = GetTime()
        local cooldown = RF.charDB.chatSoundCooldown or 5
        if not RF._lastKeywordSound or now - RF._lastKeywordSound >= cooldown then
            RF._lastKeywordSound = now
            PlaySound(SOUNDKIT.RAID_WARNING or 8959, "Master")
        end
    end
end

RF:RegisterCallback("CHAT_MSG_GUILD", function(_, message, sender, ...)
    OnGuildChat("CHAT_MSG_GUILD", message, sender, ...)
end)

RF:RegisterCallback("CHAT_MSG_OFFICER", function(_, message, sender, ...)
    OnGuildChat("CHAT_MSG_OFFICER", message, sender, ...)
end)

-- Load persisted chat — GetGuildInfo("player") may not be available
-- for several seconds after login, so retry until it works
local function RetryChatLogLoad()
    if chatLogLoaded then return end
    LoadChatLog()
    if not chatLogLoaded then
        C_Timer.After(2, RetryChatLogLoad)
    end
end

RF:RegisterCallback("ADDON_LOADED", function()
    C_Timer.After(1, RetryChatLogLoad)
end)

RF:RegisterCallback("GUILD_ROSTER_UPDATE", function()
    LoadChatLog()
end)

-- PLAYER_GUILD_UPDATE is the definitive signal that GetGuildInfo("player")
-- is now available.  This fires once, usually a few seconds after login.
RF:RegisterCallback("PLAYER_GUILD_UPDATE", function()
    LoadChatLog()
end)

RF:RegisterCallback("ROSTER_REFRESHED", function()
    LoadChatLog()
end)

-------------------------------------------------------------------------------
-- Keyword Highlight Helper
-------------------------------------------------------------------------------
local function ApplyKeywordHighlights(message)
    local keywords = RF.charDB and RF.charDB.chatKeywords
    if not keywords or #keywords == 0 then return message end

    local color = RF.charDB.chatKeywordColor or "FFFF00"
    local result = message
    for _, keyword in ipairs(keywords) do
        if keyword ~= "" then
            local lower = result:lower()
            local wLower = keyword:lower()
            local out = {}
            local pos = 1
            while true do
                local s, e = lower:find(wLower, pos, true)
                if not s then
                    out[#out + 1] = result:sub(pos)
                    break
                end
                if s > pos then
                    out[#out + 1] = result:sub(pos, s - 1)
                end
                out[#out + 1] = "|cFF" .. color .. result:sub(s, e) .. "|r"
                pos = e + 1
            end
            result = table.concat(out)
        end
    end
    return result
end

-------------------------------------------------------------------------------
-- Init Chat Tab UI
-------------------------------------------------------------------------------
function RF:InitChatTab(parent)
    T = RF.Theme

    ---------------------------------------------------------------------------
    -- Top Bar: Filter/search + channel toggle
    ---------------------------------------------------------------------------
    local topBar = CreateFrame("Frame", nil, parent)
    topBar:SetHeight(28)
    topBar:SetPoint("TOPLEFT", 8, -4)
    topBar:SetPoint("TOPRIGHT", -8, -4)

    local searchBar = RF:CreateSearchBar(topBar, {
        width = 180,
        placeholder = "Filter messages...",
        onTextChanged = function(text)
            RF:RefreshChatDisplay(parent, text)
        end,
    })
    searchBar:SetPoint("LEFT", 0, 0)

    -- Officer chat visibility toggle
    local officerPill = RF:CreatePillButton(topBar, "Officer")
    officerPill:SetPoint("LEFT", searchBar, "RIGHT", 8, 0)
    officerPill:SetActive(true)
    officerPill:SetScript("OnClick", function()
        officerPill:Toggle()
        RF:RefreshChatDisplay(parent)
    end)
    parent.showOfficer = officerPill

    -- Compact mode toggle
    local compactPill = RF:CreatePillButton(topBar, "Compact")
    compactPill:SetPoint("LEFT", officerPill, "RIGHT", 4, 0)
    compactPill:SetActive(RF.charDB and RF.charDB.chatCompactMode or false)
    compactPill:SetScript("OnClick", function()
        compactPill:Toggle()
        if RF.charDB then
            RF.charDB.chatCompactMode = compactPill:IsActive()
        end
        parent.chatFrame:SetSpacing(compactPill:IsActive() and 0 or (RF.charDB and RF.charDB.chatMessageSpacing or 2))
        RF:RefreshChatDisplay(parent)
    end)
    parent.compactMode = compactPill

    -- Open default guild chat button
    local openChatBtn = RF:CreateButton(topBar, "Open Guild Chat", 120, 22, "accent")
    openChatBtn:SetPoint("RIGHT", 0, 0)
    openChatBtn:SetScript("OnClick", function()
        if CommunitiesFrame then
            if not CommunitiesFrame:IsShown() then
                ToggleGuildFrame()
            end
            if CommunitiesFrame.ChatTab then
                CommunitiesFrame.ChatTab:Click()
            end
        elseif ToggleGuildFrame then
            ToggleGuildFrame()
        end
    end)

    -- Settings gear button + flyout
    local gearBtn = RF:CreateButton(topBar, "|TInterface\\Buttons\\UI-OptionsButton:14:14|t", 26, 22)
    gearBtn:SetPoint("RIGHT", openChatBtn, "LEFT", -4, 0)

    local settingsPanel = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    settingsPanel:SetSize(260, 270)
    settingsPanel:SetPoint("TOPRIGHT", gearBtn, "BOTTOMRIGHT", 0, -4)
    settingsPanel:SetFrameStrata("DIALOG")
    settingsPanel:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    settingsPanel:SetBackdropColor(0.10, 0.10, 0.13, 0.98)
    settingsPanel:SetBackdropBorderColor(unpack(T.border))
    settingsPanel:Hide()

    local spHeader = RF:CreateLabel(settingsPanel, "Chat Settings", RF.FONT_SIZES.small, "accent")
    spHeader:SetPoint("TOPLEFT", 10, -8)

    local spy = -26
    local SP_PAD = 10
    local SP_W = 240

    -- Helper: quick checkbox
    local function QuickCheck(y, text, getVal, setVal, onChange)
        local frame = CreateFrame("Frame", nil, settingsPanel)
        frame:SetSize(SP_W, 20)
        frame:SetPoint("TOPLEFT", SP_PAD, y)

        local check = CreateFrame("Button", nil, frame)
        check:SetSize(14, 14)
        check:SetPoint("LEFT", 0, 0)

        local bg = check:CreateTexture(nil, "BACKGROUND")
        bg:SetAllPoints()
        bg:SetColorTexture(unpack(T.bgTertiary))

        local mark = check:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        mark:SetAllPoints()
        mark:SetFont(mark:GetFont(), 10, "")
        mark:SetTextColor(unpack(T.accent))

        local label = RF:CreateLabel(frame, text, RF.FONT_SIZES.small, "textSecondary")
        label:SetPoint("LEFT", check, "RIGHT", 6, 0)

        local function Update()
            mark:SetText(getVal() and "X" or "")
        end
        local function Toggle()
            setVal(not getVal())
            Update()
            if onChange then onChange() end
        end

        check:SetScript("OnClick", Toggle)
        local btn = CreateFrame("Button", nil, frame)
        btn:SetPoint("LEFT", check, "LEFT")
        btn:SetPoint("RIGHT", frame, "RIGHT")
        btn:SetHeight(20)
        btn:SetScript("OnClick", Toggle)
        Update()

        return y - 22
    end

    -- Helper: quick stepper
    local function QuickStepper(y, text, min, max, step, getVal, setVal, fmt, onChange)
        fmt = fmt or "%.0f"
        local frame = CreateFrame("Frame", nil, settingsPanel)
        frame:SetSize(SP_W, 20)
        frame:SetPoint("TOPLEFT", SP_PAD, y)

        local label = RF:CreateLabel(frame, text, RF.FONT_SIZES.small, "textSecondary")
        label:SetPoint("LEFT", 0, 0)

        local minBtn = RF:CreateButton(frame, "-", 22, 18)
        minBtn:SetPoint("LEFT", 140, 0)

        local valLabel = RF:CreateLabel(frame, format(fmt, getVal()), RF.FONT_SIZES.small, "textPrimary")
        valLabel:SetPoint("LEFT", minBtn, "RIGHT", 4, 0)
        valLabel:SetWidth(35)
        valLabel:SetJustifyH("CENTER")

        local plusBtn = RF:CreateButton(frame, "+", 22, 18)
        plusBtn:SetPoint("LEFT", valLabel, "RIGHT", 4, 0)

        local function Refresh()
            valLabel:SetText(format(fmt, getVal()))
            if onChange then onChange() end
        end
        minBtn:SetScript("OnClick", function()
            setVal(math.max(min, getVal() - step))
            Refresh()
        end)
        plusBtn:SetScript("OnClick", function()
            setVal(math.min(max, getVal() + step))
            Refresh()
        end)

        return y - 24
    end

    -- Font size
    spy = QuickStepper(spy, "Font size", 8, 20, 1, function()
        return RF.charDB and RF.charDB.chatFontSize or 12
    end, function(v)
        if RF.charDB then RF.charDB.chatFontSize = v; RF:ApplyChatFont() end
    end, "%.0f")

    -- Message spacing
    spy = QuickStepper(spy, "Message spacing", 0, 10, 1, function()
        return RF.charDB and RF.charDB.chatMessageSpacing or 2
    end, function(v)
        if RF.charDB then RF.charDB.chatMessageSpacing = v end
    end, "%.0f", function()
        local compact = RF.charDB and RF.charDB.chatCompactMode
        if not compact then
            parent.chatFrame:SetSpacing(RF.charDB and RF.charDB.chatMessageSpacing or 2)
        end
    end)

    -- Timestamp format dropdown
    do
        local frame = CreateFrame("Frame", nil, settingsPanel)
        frame:SetSize(SP_W, 20)
        frame:SetPoint("TOPLEFT", SP_PAD, spy)

        local label = RF:CreateLabel(frame, "Timestamps", RF.FONT_SIZES.small, "textSecondary")
        label:SetPoint("LEFT", 0, 0)

        local tsItems = {}
        for _, entry in ipairs(RF.CHAT_TIMESTAMP_FORMATS) do
            tinsert(tsItems, { label = entry.label, value = entry.value })
        end
        local dropdown = RF:CreateDropdown(frame, {
            width = 100,
            defaultText = RF.charDB and RF.charDB.chatTimestampFormat or "12h",
            defaultValue = RF.charDB and RF.charDB.chatTimestampFormat or "12h",
            items = tsItems,
            onChanged = function(val)
                if RF.charDB then
                    RF.charDB.chatTimestampFormat = val
                    RF:RefreshChatDisplay(parent)
                end
            end,
        })
        dropdown:SetPoint("LEFT", 140, 0)
        spy = spy - 28
    end

    -- Font outline dropdown
    do
        local frame = CreateFrame("Frame", nil, settingsPanel)
        frame:SetSize(SP_W, 20)
        frame:SetPoint("TOPLEFT", SP_PAD, spy)

        local label = RF:CreateLabel(frame, "Font outline", RF.FONT_SIZES.small, "textSecondary")
        label:SetPoint("LEFT", 0, 0)

        local olItems = {}
        for _, entry in ipairs(RF.CHAT_OUTLINES) do
            tinsert(olItems, { label = entry.label, value = entry.value })
        end
        local curOutline = RF.charDB and RF.charDB.chatFontOutline or ""
        local curLabel = "None"
        for _, entry in ipairs(RF.CHAT_OUTLINES) do
            if entry.value == curOutline then curLabel = entry.label; break end
        end
        local dropdown = RF:CreateDropdown(frame, {
            width = 100,
            defaultText = curLabel,
            defaultValue = curOutline,
            items = olItems,
            onChanged = function(val)
                if RF.charDB then
                    RF.charDB.chatFontOutline = val
                    RF:ApplyChatFont()
                end
            end,
        })
        dropdown:SetPoint("LEFT", 140, 0)
        spy = spy - 28
    end

    -- Checkboxes
    spy = QuickCheck(spy, "Sound on keyword mention", function()
        return RF.charDB and RF.charDB.chatSoundAlerts or false
    end, function(v)
        if RF.charDB then RF.charDB.chatSoundAlerts = v end
    end)

    spy = QuickCheck(spy, "Show date separators", function()
        return RF.charDB and RF.charDB.chatShowDateSeparators ~= false
    end, function(v)
        if RF.charDB then RF.charDB.chatShowDateSeparators = v end
    end, function() RF:RefreshChatDisplay(parent) end)

    spy = QuickCheck(spy, "Show unread badge", function()
        return RF.charDB and RF.charDB.chatShowUnreadBadge ~= false
    end, function(v)
        if RF.charDB then RF.charDB.chatShowUnreadBadge = v end
        RF:UpdateChatUnreadBadge()
    end)

    spy = QuickCheck(spy, "New message indicator", function()
        return RF.charDB and RF.charDB.chatShowNewMsgIndicator ~= false
    end, function(v)
        if RF.charDB then RF.charDB.chatShowNewMsgIndicator = v end
    end)

    spy = QuickCheck(spy, "Chat background", function()
        return RF.charDB and RF.charDB.chatShowBackground ~= false
    end, function(v)
        if RF.charDB then RF.charDB.chatShowBackground = v end
        if parent.chatBg then
            if v then parent.chatBg:Show() else parent.chatBg:Hide() end
        end
    end)

    spy = QuickCheck(spy, "Pause scroll when reading", function()
        return RF.charDB and RF.charDB.chatAutoScroll ~= false
    end, function(v)
        if RF.charDB then RF.charDB.chatAutoScroll = v end
    end)

    -- Resize panel to fit content
    settingsPanel:SetHeight(math.abs(spy) + 16)

    gearBtn:SetScript("OnClick", function()
        if settingsPanel:IsShown() then
            settingsPanel:Hide()
        else
            settingsPanel:Show()
        end
    end)

    -- Close panel when clicking elsewhere
    settingsPanel:SetScript("OnShow", function()
        settingsPanel:SetFrameLevel(parent:GetFrameLevel() + 20)
    end)

    -- Refresh button
    local refreshBtn = RF:CreateButton(topBar, "Refresh", 60, 22)
    refreshBtn:SetPoint("RIGHT", gearBtn, "LEFT", -4, 0)
    refreshBtn:SetScript("OnClick", function()
        LoadChatLog()
        RF:RefreshChatDisplay(parent)
        parent.chatFrame:ScrollToBottom()
    end)

    -- Scroll to bottom
    local scrollBtn = RF:CreateButton(topBar, "Latest", 55, 22)
    scrollBtn:SetPoint("RIGHT", refreshBtn, "LEFT", -4, 0)
    scrollBtn:SetScript("OnClick", function()
        parent.chatFrame:ScrollToBottom()
        -- Hide new message bar when scrolling to bottom
        if parent.newMsgBar then
            RF._chatNewMsgCount = 0
            parent.newMsgBar:Hide()
        end
    end)

    ---------------------------------------------------------------------------
    -- Chat Background Panel
    ---------------------------------------------------------------------------
    if RF.charDB and RF.charDB.chatShowBackground ~= false then
        local chatBg = CreateFrame("Frame", nil, parent, "BackdropTemplate")
        chatBg:SetPoint("TOPLEFT", topBar, "BOTTOMLEFT", 0, -4)
        chatBg:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -8, 36)
        chatBg:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            edgeSize = 1,
        })
        chatBg:SetBackdropColor(unpack(T.bgSecondary))
        chatBg:SetBackdropBorderColor(unpack(T.border))
        parent.chatBg = chatBg
    end

    ---------------------------------------------------------------------------
    -- Chat Display Area
    ---------------------------------------------------------------------------
    local chatFrame = CreateFrame("ScrollingMessageFrame", nil, parent)
    chatFrame:SetPoint("TOPLEFT", topBar, "BOTTOMLEFT", 0, -4)
    chatFrame:SetPoint("BOTTOMRIGHT", -8, 36)
    local fontPath, _, fontFlags = ChatFontNormal:GetFont()
    -- Temporary font so the frame is valid; real font applied via ApplyChatFont()
    chatFrame:SetFont(fontPath, 12, "")
    chatFrame:SetTextColor(unpack(T.textPrimary))
    chatFrame:SetJustifyH("LEFT")
    chatFrame:SetMaxLines(MAX_MESSAGES)
    chatFrame:SetFading(false)
    chatFrame:SetSpacing(RF.charDB and RF.charDB.chatMessageSpacing or 2)
    chatFrame:EnableMouseWheel(true)
    chatFrame:SetScript("OnMouseWheel", function(self, delta)
        if delta > 0 then
            self:ScrollUp()
        else
            self:ScrollDown()
        end
        -- Hide new message bar when scrolled to bottom
        if self:AtBottom() and parent.newMsgBar then
            RF._chatNewMsgCount = 0
            parent.newMsgBar:Hide()
        end
    end)
    chatFrame:SetHyperlinksEnabled(true)
    chatFrame:SetScript("OnHyperlinkClick", function(self, link, text, button)
        local linkType, linkData = link:match("^(%a+):(.+)")
        if linkType == "url" then
            RF:ShowCopyPopup(linkData)
        elseif linkType == "tstamp" then
            -- Full timestamp copy on click
            local epoch = tonumber(linkData)
            if epoch then
                RF:ShowCopyPopup(date("%Y-%m-%d %H:%M:%S", epoch))
            end
        elseif linkType == "player" then
            local name = linkData:match("([^:]+)")
            if button == "RightButton" then
                local items = {
                    { text = name, func = function() end, disabled = true },
                    { separator = true },
                    { text = "Reply (@" .. name .. ")", func = function()
                        if parent.inputBox then
                            local cur = parent.inputBox:GetText()
                            local prefix = cur ~= "" and (cur .. " ") or ""
                            parent.inputBox:SetText(prefix .. "@" .. name .. " ")
                            parent.inputBox:SetFocus()
                            parent.inputBox:SetCursorPosition(#parent.inputBox:GetText())
                        end
                    end },
                    { text = "Whisper", func = function()
                        ChatFrame_OpenChat("/w " .. name .. " ")
                    end },
                    { text = "Invite", func = function()
                        local member = RF:GetMemberByName(name)
                        C_PartyInfo.InviteUnit(member and member.fullName or name)
                    end },
                    { text = "Inspect", func = function()
                        local member = RF:GetMemberByName(name)
                        if member and member.isOnline then
                            NotifyInspect(name)
                        else
                            RF:Print(name .. " must be online and nearby to inspect.")
                        end
                    end },
                }
                RF:ShowContextMenu(self, items)
            else
                -- Left-click: insert @Name into input box
                if parent.inputBox then
                    local cur = parent.inputBox:GetText()
                    local prefix = cur ~= "" and (cur .. " ") or ""
                    parent.inputBox:SetText(prefix .. "@" .. name .. " ")
                    parent.inputBox:SetFocus()
                    parent.inputBox:SetCursorPosition(#parent.inputBox:GetText())
                end
            end
        else
            -- Item, achievement, spell, etc. — shift-click inserts link
            if IsModifiedClick("CHATLINK") and ChatEdit_InsertLink then
                local fullLink = "|H" .. link .. "|h" .. text .. "|h"
                ChatEdit_InsertLink(fullLink)
            elseif ChatFrame_OnHyperlinkShow then
                ChatFrame_OnHyperlinkShow(ChatFrame1, link, text, button)
            end
        end
    end)
    chatFrame:SetScript("OnHyperlinkEnter", function(self, link)
        if link:match("^url:") or link:match("^player:") then return end
        if link:match("^tstamp:") then
            local epoch = tonumber(link:match("^tstamp:(%d+)"))
            if epoch then
                GameTooltip:SetOwner(self, "ANCHOR_CURSOR")
                GameTooltip:AddLine(date("%A, %B %d, %Y", epoch), 1, 1, 1)
                GameTooltip:AddLine(date("%I:%M:%S %p", epoch), 0.7, 0.7, 0.7)
                GameTooltip:Show()
            end
            return
        end
        GameTooltip:SetOwner(self, "ANCHOR_CURSOR")
        GameTooltip:SetHyperlink(link)
        GameTooltip:Show()
    end)
    chatFrame:SetScript("OnHyperlinkLeave", function()
        GameTooltip:Hide()
    end)

    -- Right-click on empty chat area: copy last message / export
    chatFrame:SetScript("OnMouseUp", function(self, button)
        if button == "RightButton" then
            local items = {
                { text = "Copy Last Message", func = function()
                    local last = chatLog[#chatLog]
                    if last then
                        RF:ShowCopyPopup(last.message)
                    end
                end },
                { text = "Export Chat Log", func = function()
                    local lines = {}
                    for _, entry in ipairs(chatLog) do
                        local prefix = entry.type == "officer" and "[O] " or ""
                        tinsert(lines, format("[%s] %s%s: %s",
                            date("%Y-%m-%d %H:%M:%S", entry.time),
                            prefix, entry.sender, entry.message))
                    end
                    if #lines > 0 then
                        RF:ShowCopyPopup(table.concat(lines, "\n"))
                    else
                        RF:Print("No chat messages to export.")
                    end
                end },
            }
            RF:ShowContextMenu(self, items)
        end
    end)

    parent.chatFrame = chatFrame
    RF.chatFrame = chatFrame
    RF:ApplyChatFont()

    ---------------------------------------------------------------------------
    -- New Messages Indicator Bar
    ---------------------------------------------------------------------------
    RF._chatNewMsgCount = 0

    local newMsgBar = CreateFrame("Button", nil, parent, "BackdropTemplate")
    newMsgBar:SetHeight(20)
    newMsgBar:SetPoint("BOTTOMLEFT", chatFrame, "BOTTOMLEFT", 0, 0)
    newMsgBar:SetPoint("BOTTOMRIGHT", chatFrame, "BOTTOMRIGHT", 0, 0)
    newMsgBar:SetFrameLevel(chatFrame:GetFrameLevel() + 5)
    newMsgBar:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
    })
    newMsgBar:SetBackdropColor(unpack(T.accent))

    local newMsgText = newMsgBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    newMsgText:SetFont(fontPath, 10, "OUTLINE")
    newMsgText:SetPoint("CENTER")
    newMsgText:SetTextColor(1, 1, 1)
    newMsgBar.text = newMsgText
    newMsgBar:Hide()
    parent.newMsgBar = newMsgBar

    newMsgBar:SetScript("OnClick", function()
        parent.chatFrame:ScrollToBottom()
        RF._chatNewMsgCount = 0
        newMsgBar:Hide()
    end)

    ---------------------------------------------------------------------------
    -- Bottom Bar: Channel toggle + message input + send
    ---------------------------------------------------------------------------
    local inputBar = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    inputBar:SetHeight(30)
    inputBar:SetPoint("BOTTOMLEFT", 8, 4)
    inputBar:SetPoint("BOTTOMRIGHT", -8, 4)
    inputBar:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    inputBar:SetBackdropColor(unpack(T.bgTertiary))
    inputBar:SetBackdropBorderColor(unpack(T.border))

    -- Sticky channel: restore from saved setting
    chatChannel = RF.charDB and RF.charDB.chatStickyChannel or "guild"

    -- Channel toggle button (Guild / Officer)
    local channelBtn = CreateFrame("Button", nil, inputBar)
    channelBtn:SetSize(60, 24)
    channelBtn:SetPoint("LEFT", 4, 0)

    local channelBg = channelBtn:CreateTexture(nil, "BACKGROUND")
    channelBg:SetAllPoints()
    channelBg:SetColorTexture(unpack(T.bgSecondary))

    local channelLabel = channelBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    channelLabel:SetFont(fontPath, RF.FONT_SIZES.small, "")
    channelLabel:SetPoint("CENTER")
    parent.channelLabel = channelLabel

    local function UpdateChannelDisplay()
        if chatChannel == "officer" then
            channelLabel:SetText("/o")
            channelLabel:SetTextColor(1.0, 0.42, 0.42)
        else
            channelLabel:SetText("/g")
            channelLabel:SetTextColor(0.30, 0.90, 0.30)
        end
    end
    UpdateChannelDisplay()

    channelBtn:SetScript("OnClick", function()
        chatChannel = (chatChannel == "guild") and "officer" or "guild"
        if RF.charDB then RF.charDB.chatStickyChannel = chatChannel end
        UpdateChannelDisplay()
    end)

    channelBtn:SetScript("OnEnter", function()
        GameTooltip:SetOwner(channelBtn, "ANCHOR_TOP")
        GameTooltip:AddLine("Click to toggle Guild / Officer chat")
        GameTooltip:Show()
    end)
    channelBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Message input
    local inputBox = CreateFrame("EditBox", nil, inputBar)
    inputBox:SetPoint("LEFT", channelBtn, "RIGHT", 6, 0)
    inputBox:SetPoint("RIGHT", -110, 0)
    inputBox:SetHeight(24)
    inputBox:SetFontObject("ChatFontNormal")
    inputBox:SetFont(fontPath, RF.FONT_SIZES.normal, "")
    inputBox:SetTextColor(unpack(T.textPrimary))
    inputBox:SetAutoFocus(false)
    inputBox:SetMaxLetters(255)
    parent.inputBox = inputBox

    -- Character count label
    local charCount = inputBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    charCount:SetFont(fontPath, RF.FONT_SIZES.small, "")
    charCount:SetPoint("RIGHT", -62, 0)
    charCount:SetTextColor(unpack(T.textMuted))
    charCount:SetText("")

    -- Placeholder
    local inputPlaceholder = inputBox:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    inputPlaceholder:SetFont(fontPath, RF.FONT_SIZES.normal, "")
    inputPlaceholder:SetPoint("LEFT", 0, 0)
    inputPlaceholder:SetTextColor(unpack(T.textMuted))
    inputPlaceholder:SetText("Type a message...")

    inputBox:SetScript("OnTextChanged", function(self, userInput)
        local text = self:GetText()
        local len = #text
        if len > 0 then
            inputPlaceholder:Hide()
            charCount:SetText(len .. "/255")
            if len >= 230 then
                charCount:SetTextColor(0.90, 0.25, 0.25)
            elseif len >= 200 then
                charCount:SetTextColor(1.00, 0.75, 0.20)
            else
                charCount:SetTextColor(unpack(T.textMuted))
            end
        else
            inputPlaceholder:Show()
            charCount:SetText("")
        end
        -- Reset tab-complete on user input
        if userInput then
            tabState.active = false
        end
    end)

    -- Send function
    local function SendMessage()
        local text = inputBox:GetText():trim()
        if text == "" then return end

        -- Store in sent history
        tinsert(sentHistory, text)
        if #sentHistory > MAX_SENT_HISTORY then
            tremove(sentHistory, 1)
        end
        sentHistoryIdx = 0

        if chatChannel == "officer" then
            C_ChatInfo.SendChatMessage(text, "OFFICER")
        else
            C_ChatInfo.SendChatMessage(text, "GUILD")
        end

        inputBox:SetText("")
        inputBox:SetFocus()
    end

    inputBox:SetScript("OnEnterPressed", function()
        SendMessage()
    end)

    inputBox:SetScript("OnEscapePressed", function(self)
        self:SetText("")
        self:ClearFocus()
    end)

    -- Tab-complete: guild member names (Ctrl+Tab to avoid taint)
    -- Note: OnTabPressed on EditBox can taint SendChatMessage, so we use
    -- a timer-deferred approach triggered by Ctrl+Space instead.
    local function DoTabComplete()
        local text = inputBox:GetText()
        local cursor = inputBox:GetCursorPosition()

        if not tabState.active then
            local before = text:sub(1, cursor)
            local wordStart = before:match(".*()%s") or 0
            wordStart = wordStart + 1
            local prefix = before:sub(wordStart)

            if prefix == "" or #prefix < 1 then return end

            tabState.prefix = prefix:lower()
            tabState.replaceStart = wordStart
            tabState.replaceEnd = cursor
            tabState.matches = {}
            tabState.idx = 0

            local searchPrefix = tabState.prefix
            local atPrefix = searchPrefix:match("^@(.+)")
            if atPrefix then searchPrefix = atPrefix end

            if RF.rosterData then
                for _, member in ipairs(RF.rosterData) do
                    if member.name:lower():sub(1, #searchPrefix) == searchPrefix then
                        local insertName = atPrefix and ("@" .. member.name) or member.name
                        tinsert(tabState.matches, insertName)
                    end
                end
                table.sort(tabState.matches)
            end

            tabState.active = true
        end

        if #tabState.matches == 0 then
            tabState.active = false
            return
        end

        tabState.idx = (tabState.idx % #tabState.matches) + 1
        local match = tabState.matches[tabState.idx]
        local before = text:sub(1, tabState.replaceStart - 1)
        local after = text:sub(tabState.replaceEnd + 1)
        local newText = before .. match .. after
        tabState.replaceEnd = tabState.replaceStart + #match - 1

        inputBox:SetText(newText)
        inputBox:SetCursorPosition(tabState.replaceStart - 1 + #match)
    end

    inputBox:SetScript("OnTabPressed", function(self)
        -- Defer to next frame to avoid tainting the EditBox secure input chain
        C_Timer.After(0, DoTabComplete)
    end)

    -- Focus highlight
    inputBox:SetScript("OnEditFocusGained", function()
        inputBar:SetBackdropBorderColor(unpack(T.accent))
    end)
    inputBox:SetScript("OnEditFocusLost", function()
        inputBar:SetBackdropBorderColor(unpack(T.border))
    end)

    -- Send button
    local sendBtn = RF:CreateButton(inputBar, "Send", 55, 24, "accent")
    sendBtn:SetPoint("RIGHT", -3, 0)
    sendBtn:SetScript("OnClick", function()
        SendMessage()
    end)

    ---------------------------------------------------------------------------
    -- Refresh Display
    ---------------------------------------------------------------------------
    -- Format a single chat line (shared by refresh and append)
    local function FormatChatLine(entry)
        local compact = RF.charDB and RF.charDB.chatCompactMode
        local prefix = entry.type == "officer" and "|cFFFF6B6B[O]|r " or ""

        local senderText = entry.sender
        local memberData = RF:GetMemberByName(entry.sender)
        if memberData then
            senderText = RF:GetClassColoredName(entry.sender, memberData.classFile)
        end
        local senderLink = format("|Hplayer:%s|h%s|h", entry.sender, senderText)

        local displayMsg = ApplyKeywordHighlights(entry.message)
        displayMsg = RF:WrapURLs(displayMsg)
        displayMsg = ReplaceEmojis(displayMsg)

        if compact then
            return format("%s%s|cFFFFFFFF:|r |cFF40FF40%s|r", prefix, senderLink, displayMsg)
        else
            local timestamp = RF:FormatTimestampChat(entry.time)
            local tsLink = format("|Htstamp:%d|h%s|h", entry.time, timestamp)
            return format("|cFF888888%s|r %s%s|cFFFFFFFF:|r |cFF40FF40%s|r", tsLink, prefix, senderLink, displayMsg)
        end
    end

    function RF:RefreshChatDisplay(tab, filterText)
        local cf = tab.chatFrame
        if not cf then return end
        cf:Clear()

        filterText = filterText and filterText:lower() or ""
        local showOfficer = tab.showOfficer and tab.showOfficer:IsActive()
        local lastDate = nil

        for _, entry in ipairs(chatLog) do
            local show = true

            if entry.type == "officer" and not showOfficer then
                show = false
            end

            if show and filterText ~= "" then
                if not entry.sender:lower():find(filterText, 1, true)
                   and not entry.message:lower():find(filterText, 1, true) then
                    show = false
                end
            end

            if show then
                -- Date separator
                if RF.charDB and RF.charDB.chatShowDateSeparators ~= false then
                    local msgDate = date("%Y-%m-%d", entry.time)
                    if lastDate and msgDate ~= lastDate then
                        local sepLine = format("|cFF555555\226\148\128\226\148\128 %s \226\148\128\226\148\128|r", date("%B %d, %Y", entry.time))
                        cf:AddMessage(sepLine)
                    end
                    lastDate = msgDate
                end

                cf:AddMessage(FormatChatLine(entry))
            end
        end

        -- Track the last date for append
        if #chatLog > 0 then
            RF._chatLastDate = date("%Y-%m-%d", chatLog[#chatLog].time)
        else
            RF._chatLastDate = nil
        end
    end

    -- Append new messages live (without full refresh for performance)
    local function AppendLatestMessage()
        if not parent:IsShown() then return end
        local entry = chatLog[#chatLog]
        if not entry then return end

        local showOfficer = parent.showOfficer and parent.showOfficer:IsActive()
        if entry.type == "officer" and not showOfficer then return end

        -- Track whether we were at bottom before adding
        local wasAtBottom = parent.chatFrame:AtBottom()

        -- Date separator
        if RF.charDB and RF.charDB.chatShowDateSeparators ~= false then
            local msgDate = date("%Y-%m-%d", entry.time)
            if RF._chatLastDate and msgDate ~= RF._chatLastDate then
                parent.chatFrame:AddMessage(format("|cFF555555\226\148\128\226\148\128 %s \226\148\128\226\148\128|r", date("%B %d, %Y", entry.time)))
            end
            RF._chatLastDate = msgDate
        end

        parent.chatFrame:AddMessage(FormatChatLine(entry))

        -- Auto-scroll: preserve position when scrolled up
        if not wasAtBottom and RF.charDB and RF.charDB.chatAutoScroll ~= false then
            local offset = parent.chatFrame:GetScrollOffset()
            parent.chatFrame:SetScrollOffset(offset + 1)
        end

        -- New message indicator when scrolled up
        if not parent.chatFrame:AtBottom() and RF.charDB and RF.charDB.chatShowNewMsgIndicator ~= false then
            RF._chatNewMsgCount = (RF._chatNewMsgCount or 0) + 1
            if parent.newMsgBar then
                parent.newMsgBar.text:SetText(RF._chatNewMsgCount .. " new message" .. (RF._chatNewMsgCount == 1 and "" or "s") .. " \226\150\188")
                parent.newMsgBar:Show()
            end
        end
    end

    -- Refresh on tab switch
    RF:RegisterCallback("SWITCH_TAB", function(_, key)
        if key == "Chat" then
            LoadChatLog()
            RF:ApplyChatFont()
            local compact = RF.charDB and RF.charDB.chatCompactMode
            parent.chatFrame:SetSpacing(compact and 0 or (RF.charDB and RF.charDB.chatMessageSpacing or 2))
            RF:RefreshChatDisplay(parent)
            parent.chatFrame:ScrollToBottom()

            -- Clear unread badge
            RF._chatUnreadCount = 0
            RF:UpdateChatUnreadBadge()

            -- Clear new message indicator
            RF._chatNewMsgCount = 0
            if parent.newMsgBar then
                parent.newMsgBar:Hide()
            end
        end
    end)

    -- Refresh display when persisted chat finishes loading
    RF:RegisterCallback("CHAT_LOG_LOADED", function()
        RF:RefreshChatDisplay(parent)
        parent.chatFrame:ScrollToBottom()
    end)

    -- Live append new messages (deferred slightly to ensure OnGuildChat stores the entry first)
    RF:RegisterCallback("CHAT_MSG_GUILD", function()
        C_Timer.After(0, function() AppendLatestMessage() end)
    end)
    RF:RegisterCallback("CHAT_MSG_OFFICER", function()
        C_Timer.After(0, function() AppendLatestMessage() end)
    end)

    -- Initial display
    RF:RefreshChatDisplay(parent)
end
