-- LongChat tabbed composer workspace.

local LC = _G.LongChat
local UI = LC and LC.UI
if not UI then
  return
end

local TAB_COMPOSE = "compose"
local TAB_NOTES = "notes"
local JOURNAL_TEMPLATE_TITLE = "New note"
local JOURNAL_COLORS = {
  { hex = "ffd84a", r = 1.00, g = 0.85, b = 0.29 },
  { hex = "ff7a3d", r = 1.00, g = 0.48, b = 0.24 },
  { hex = "ff4d6d", r = 1.00, g = 0.30, b = 0.43 },
  { hex = "c878ff", r = 0.78, g = 0.47, b = 1.00 },
  { hex = "69a7ff", r = 0.41, g = 0.65, b = 1.00 },
  { hex = "53d6d1", r = 0.33, g = 0.84, b = 0.82 },
  { hex = "72e06a", r = 0.45, g = 0.88, b = 0.42 },
  { hex = "ffffff", r = 1.00, g = 1.00, b = 1.00 },
}

local function panel_backdrop()
  return {
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    tile = false,
    edgeSize = 10,
    insets = { left = 2, right = 2, top = 2, bottom = 2 },
  }
end

local tintedFrames = setmetatable({}, { __mode = "k" })

local function composer_theme()
  if UI and UI.Editor then
    return UI:Editor().theme or "dark"
  end
  return "dark"
end

local function composer_opacity()
  if UI and UI.Editor then
    return UI:Editor().opacity or 1
  end
  return 1
end

local function theme_frame_color(theme, r, g, b, a)
  local alpha = (a or 1) * composer_opacity()
  local light = math.min(0.09, math.max(0, ((r or 0) + (g or 0) + (b or 0)) / 3))

  if theme == "custom" and UI and UI.Editor then
    local color = UI:Editor().customColor or {}
    local bgR = tonumber(color.r) or 0.08
    local bgG = tonumber(color.g) or 0.10
    local bgB = tonumber(color.b) or 0.12
    local borderR = math.min(1, bgR + 0.18)
    local borderG = math.min(1, bgG + 0.18)
    local borderB = math.min(1, bgB + 0.18)
    return bgR, bgG, bgB, alpha, borderR, borderG, borderB, math.max(0.35, alpha)
  end

  if theme == "parchment" then
    local bgR = 0.30 + (light * 1.90)
    local bgG = 0.235 + (light * 1.55)
    local bgB = 0.145 + (light * 1.05)
    local borderR = 0.63 + (light * 1.60)
    local borderG = 0.45 + (light * 1.25)
    local borderB = 0.22 + (light * 0.90)
    return bgR, bgG, bgB, alpha, borderR, borderG, borderB, math.max(0.35, alpha)
  end

  local bgR = 0.075 + (light * 1.10)
  local bgG = 0.050 + (light * 0.78)
  local bgB = 0.020 + (light * 0.38)
  local borderR = 0.42 + (light * 2.20)
  local borderG = 0.30 + (light * 1.70)
  local borderB = 0.12 + (light * 0.90)
  return bgR, bgG, bgB, alpha, borderR, borderG, borderB, math.max(0.35, alpha)
end

local function tint(frame, r, g, b, a)
  tintedFrames[frame] = { r, g, b, a }
  local theme = composer_theme()
  local bgR, bgG, bgB, bgA, borderR, borderG, borderB, borderA
  if theme ~= "dark" then
    bgR, bgG, bgB, bgA, borderR, borderG, borderB, borderA = theme_frame_color(theme, r, g, b, a)
  else
    bgR, bgG, bgB, bgA = r, g, b, (a or 1) * composer_opacity()
    borderR, borderG, borderB, borderA = r + 0.16, g + 0.16, b + 0.16, math.max(0.35, bgA)
  end
  frame:SetBackdropColor(bgR, bgG, bgB, bgA)
  frame:SetBackdropBorderColor(borderR, borderG, borderB, borderA)
end

function UI:RefreshTheme()
  for frame, color in pairs(tintedFrames) do
    if frame and frame.SetBackdropColor then
      tint(frame, color[1], color[2], color[3], color[4])
    end
  end
end

function UI:RefreshTextColor()
  local color = self:Editor().textColor or {}
  local r = color.r or 1
  local g = color.g or 1
  local b = color.b or 1
  if self.editBox and self.editBox.SetTextColor then
    self.editBox:SetTextColor(r, g, b, 1)
  end
  if self.notesBox and self.notesBox.SetTextColor then
    self.notesBox:SetTextColor(r, g, b, 1)
  end
end

function UI:RefreshComposerFontScale()
  local path, size, flags = ChatFontNormal and ChatFontNormal:GetFont()
  if not path then
    return
  end

  local scale = tonumber(self:Editor().fontScale) or 1
  local fontSize = math.max(8, math.floor(((tonumber(size) or 12) * scale) + 0.5))
  local function apply_font(editbox)
    if not editbox or not editbox.SetFont then
      return
    end
    editbox:SetFont(path, fontSize, flags or "")
  end
  apply_font(self.editBox)
  apply_font(self.notesBox)
  if self.RefreshSpellcheckOverlay then
    self:RefreshSpellcheckOverlay()
  end
end

local function put_label(parent, text, template)
  local label = parent:CreateFontString(nil, "ARTWORK", template or "GameFontHighlightSmall")
  label:SetJustifyH("LEFT")
  label:SetText(text or "")
  return label
end

local function plain_color_text(text)
  text = tostring(text or "")
  text = text:gsub("|cff%x%x%x%x%x%x", "")
  text = text:gsub("|r", "")
  return text
end

local function clean_journal_title(title)
  title = tostring(title or ""):gsub("^%s+", ""):gsub("%s+$", "")
  if title == "" then
    title = "Untitled event"
  end
  if #title > 54 then
    title = title:sub(1, 51) .. "..."
  end
  return title
end

local function journal_time()
  return date and (date("%m/%d/%y %I:%M %p") .. " LT") or ""
end

local function editbox_selection(editbox)
  if not editbox then
    return nil
  end

  local left, right
  if type(editbox.GetSelection) == "function" then
    left, right = editbox:GetSelection()
  elseif type(editbox.GetTextSelection) == "function" then
    left, right = editbox:GetTextSelection()
  elseif type(editbox.GetHighlightText) == "function" then
    left, right = editbox:GetHighlightText()
  end

  left = tonumber(left)
  right = tonumber(right)
  if not left or not right or left == right then
    return nil
  end

  if left > right then
    left, right = right, left
  end
  return left + 1, right
end

local function ensure_journal_rename_dialog()
  if not StaticPopupDialogs or StaticPopupDialogs.LONGCHAT_JOURNAL_RENAME then
    return
  end

  StaticPopupDialogs.LONGCHAT_JOURNAL_RENAME = {
    text = "Rename this RP journal entry",
    button1 = SAVE or "Save",
    button2 = CANCEL or "Cancel",
    hasEditBox = true,
    maxLetters = 54,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    OnAccept = function(self, data)
      data = data or self.data
      if not data or not data.ui or not data.index then
        return
      end
      local editbox = self.editBox or self.EditBox
      local title = editbox and editbox:GetText() or ""
      data.ui:RenameJournalEntry(data.index, title)
    end,
    OnShow = function(self, data)
      data = data or self.data or {}
      local editbox = self.editBox or self.EditBox
      if editbox then
        editbox:SetText(data.defaultTitle or "")
        editbox:HighlightText()
        editbox:SetFocus()
      end
    end,
    EditBoxOnEnterPressed = function(self, data)
      data = data or self:GetParent().data
      if not data or not data.ui or not data.index then
        return
      end
      local parent = self:GetParent()
      data.ui:RenameJournalEntry(data.index, self:GetText() or "")
      parent:Hide()
    end,
  }
end

local function ensure_journal_delete_dialog()
  if not StaticPopupDialogs or StaticPopupDialogs.LONGCHAT_JOURNAL_DELETE then
    return
  end

  StaticPopupDialogs.LONGCHAT_JOURNAL_DELETE = {
    text = "Delete this RP journal entry?\n\n%s",
    button1 = YES or "Yes",
    button2 = NO or "No",
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    OnAccept = function(self, data)
      data = data or self.data
      if data and data.ui and data.index then
        data.ui:DeleteJournalEntry(data.index)
      end
    end,
  }
end

function UI:RefreshChannelMenu()
  local control = self.channelDropdown
  if control and control.frame then
    UIDropDownMenu_SetText(control.frame, self:LabelFor(control.value))
  end
end

function UI:SetChannel(channel)
  local editor = self:Editor()
  editor.channel = channel or "SAY"
  if self.channelDropdown then
    self.channelDropdown.value = editor.channel
  end
  self:RefreshChannelMenu()
  self:ApplyComposerSplitMarkers(self.editBox, true)
  self:RefreshCharacterCounter()
end

function UI:CreateChannelMenu(parent)
  local menuFrame = CreateFrame("Frame", "LongChatComposerChannelMenu", parent, "UIDropDownMenuTemplate")

  local control = {
    frame = menuFrame,
    value = self:Editor().channel,
  }

  function control:GetValue()
    return self.value
  end

  function control:SetValue(value)
    UI:SetChannel(value)
  end

  UIDropDownMenu_SetWidth(menuFrame, 110)
  UIDropDownMenu_Initialize(menuFrame, function(_, level)
    for _, channel in ipairs(UI.CHANNEL_ORDER) do
      local info = UIDropDownMenu_CreateInfo()
      info.text = UI:LabelFor(channel)
      info.checked = control.value == channel
      info.func = function()
        control:SetValue(channel)
        UI:SetStatus("Channel set to " .. UI:LabelFor(channel) .. ".")
      end
      UIDropDownMenu_AddButton(info, level)
    end
  end)

  self.channelDropdown = control
  self:RefreshChannelMenu()
  return control
end

function UI:InsertText(text)
  if not self.editBox or not text then
    return
  end

  local cursor = self.editBox:GetCursorPosition() or 0
  local current = self.editBox:GetText() or ""
  self.editBox:SetText(current:sub(1, cursor) .. text .. current:sub(cursor + 1))
  self.editBox:SetCursorPosition(cursor + #text)
  self.editBox:SetFocus()
  self:RememberDraft()
end

function UI:BuildMarkerDock(parent)
  local dock = CreateFrame("Frame", nil, parent, "BackdropTemplate")
  dock:SetPoint("TOPLEFT", parent, "TOPRIGHT", 8, 0)
  dock:SetPoint("BOTTOMLEFT", parent, "BOTTOMRIGHT", 8, 0)
  dock:SetWidth(42)
  dock:SetBackdrop(panel_backdrop())
  tint(dock, 0.025, 0.035, 0.045, 0.70)

  local label = put_label(dock, "RT", "GameFontNormalSmall")
  label:SetPoint("TOP", dock, "TOP", 0, -8)

  local previous = label
  for i = 1, 8 do
    local marker = CreateFrame("Button", nil, dock, "BackdropTemplate")
    marker:SetSize(26, 26)
    marker:SetBackdrop(panel_backdrop())
    tint(marker, 0.06, 0.06, 0.065, 0.78)
    marker:SetPoint("TOP", previous, "BOTTOM", 0, previous == label and -8 or -5)
    marker.icon = marker:CreateTexture(nil, "ARTWORK")
    marker.icon:SetPoint("CENTER", marker, "CENTER", 0, 0)
    marker.icon:SetSize(20, 20)
    marker.icon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcon_" .. i)
    marker:SetScript("OnClick", function()
      UI:InsertText("{rt" .. i .. "}")
    end)
    marker:SetScript("OnEnter", function(self)
      self:SetBackdropBorderColor(0.95, 0.74, 0.28, 1)
    end)
    marker:SetScript("OnLeave", function(self)
      self:SetBackdropBorderColor(0.22, 0.22, 0.225, 0.9)
    end)
    previous = marker
  end
end

function UI:LoadActiveChat()
  local active = ChatEdit_GetActiveWindow and ChatEdit_GetActiveWindow()
  if active and self.editBox then
    self.editBox:SetText(active:GetText() or "")
    self.editBox:SetFocus()
    self:RememberDraft()
    self:SetTab(TAB_COMPOSE)
    self:SetStatus("Loaded the active chat box.")
  end
end

function UI:ClearComposer()
  if self.editBox then
    self.editBox:SetText("")
    self.editBox:SetFocus()
    self:RememberDraft()
    self:RefreshCharacterCounter()
    self:SetStatus("Composer cleared.")
  end
end

function UI:CopyHistoryToDraft(index)
  local entry = self:Editor().history[index or 1]
  if not entry or not self.editBox then
    self:SetStatus("No history entry to load.")
    return
  end

  self.editBox:SetText(entry.text or "")
  self:SetChannel(entry.channel or self:Editor().channel)
  self:RememberDraft()
  self:SetTab(TAB_COMPOSE)
  self.editBox:SetFocus()
  self:SetStatus("Loaded recent history into the composer.")
end

function UI:ClearHistory()
  wipe(self:Editor().history)
  self:RefreshHistory()
  self:SetStatus("LongChat history cleared.")
end

function UI:RefreshHistory()
  local journal = self:Editor().journal
  if not self.historyBody then
    if self.historyText then
      self.historyText:SetText(self:JournalText())
    end
    return
  end

  self.journalRows = self.journalRows or {}
  for _, row in ipairs(self.journalRows) do
    row:Hide()
  end

  if self.historyText then
    self.historyText:SetShown(#journal == 0)
    self.historyText:SetText(#journal == 0 and "No notable RP events saved yet." or "")
  end

  local previous
  for index, item in ipairs(journal) do
    local entryIndex = index
    local isSelected = self.selectedJournalIndex == entryIndex
    local row = self.journalRows[index]
    if not row then
      row = self:Button(self.historyBody, "", 320, function()
      end)
      row:SetHeight(22)
      row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
      self.journalRows[index] = row
    end

    local width = self.historyBody:GetWidth() or 320
    row:SetWidth(math.max(260, width))
    row:SetText(("%s%s - %s"):format(isSelected and "> " or "", item.time and item.time ~= "" and item.time or "undated", item.title or "Event"))
    row:SetScript("OnClick", function(_, button)
      if button == "RightButton" then
        UI.selectedJournalIndex = entryIndex
        UI:ConfirmDeleteJournalEntry(entryIndex)
      else
        UI:LoadJournalEntry(entryIndex)
      end
    end)
    row:ClearAllPoints()
    if previous then
      row:SetPoint("TOPLEFT", previous, "BOTTOMLEFT", 0, -4)
    else
      row:SetPoint("TOPLEFT", self.historyBody, "TOPLEFT", 0, 0)
    end
    row:Show()
    previous = row
  end

  self.historyBody:SetHeight(math.max(120, (#journal * 26) + 8))
end

function UI:SaveJournal()
  if not self.notesBox then
    return
  end

  local text = self.notesBox:GetText() or ""
  if text:gsub("^%s+", ""):gsub("%s+$", "") == "" then
    self:SetStatus("Write a note before saving it to the journal.")
    return
  end

  self:SaveJournalWithTitle(text:match("([^\n\r]+)") or "Untitled event")
end

function UI:SaveJournalWithTitle(title)
  if not self.notesBox then
    return
  end

  local text = self.notesBox:GetText() or ""
  text = text:gsub("^%s+", ""):gsub("%s+$", "")
  if text == "" then
    self:SetStatus("Write a note before saving it to the journal.")
    return
  end

  local journal = self:Editor().journal
  local index = self.selectedJournalIndex
  if index and journal[index] then
    journal[index].text = text
    if journal[index].longChatTemplate then
      journal[index].title = clean_journal_title(title)
      journal[index].longChatTemplate = nil
    elseif not journal[index].title or journal[index].title == "" then
      journal[index].title = clean_journal_title(title)
    end
    self:RefreshHistory()
    self:SetStatus("Updated RP journal entry.")
    return
  end

  local ok = self:SaveJournalEntry(text, title)
  if not ok then
    self:SetStatus("Write a note before saving it to the journal.")
    return
  end

  self.selectedJournalIndex = 1
  self:RefreshHistory()
  self:SetStatus("Saved notable RP event.")
end

function UI:NewJournalNote()
  if not self.notesBox then
    return
  end

  local journal = self:Editor().journal
  local current = journal[self.selectedJournalIndex or 0]
  if not current or not current.longChatTemplate or (current.text or "") ~= "" then
    table.insert(journal, 1, {
      title = JOURNAL_TEMPLATE_TITLE,
      text = "",
      time = journal_time(),
      longChatTemplate = true,
    })
    while #journal > 50 do
      table.remove(journal)
    end
    self.selectedJournalIndex = 1
  end

  self.notesBox:SetText("")
  self.notesBox:SetFocus()
  self:RefreshHistory()
  self:RememberNotes()
  self:SetStatus("Created a new RP journal note.")
end

function UI:LoadJournalEntry(index)
  local entry = self:Editor().journal[index or 1]
  if not entry or not self.notesBox then
    self:SetStatus("No journal entry to load.")
    return
  end

  self.notesBox:SetText(entry.text or "")
  self.notesBox:SetFocus()
  self.selectedJournalIndex = index or 1
  self:RefreshHistory()
  self:RememberNotes()
  self:SetStatus("Loaded journal entry: " .. tostring(entry.title or "Event"))
end

function UI:PromptRenameJournalEntry(index)
  index = index or self.selectedJournalIndex
  if not index then
    self:SetStatus("Select a journal entry to rename.")
    return
  end

  local entry = self:Editor().journal[index]
  if not entry then
    self:SetStatus("Select a journal entry to rename.")
    return
  end

  self.selectedJournalIndex = index
  self:RefreshHistory()
  ensure_journal_rename_dialog()
  if StaticPopup_Show then
    StaticPopup_Show("LONGCHAT_JOURNAL_RENAME", nil, nil, {
      ui = self,
      index = index,
      defaultTitle = entry.title or "Event",
    })
    return
  end

  self:SetStatus("Rename is unavailable in this UI state.")
end

function UI:RenameJournalEntry(index, title)
  if not index then
    self:SetStatus("No journal entry to rename.")
    return
  end

  local entry = self:Editor().journal[index]
  if not entry then
    self:SetStatus("No journal entry to rename.")
    return
  end

  entry.title = clean_journal_title(title)
  entry.longChatTemplate = nil
  self.selectedJournalIndex = index
  self:RefreshHistory()
  self:SetStatus("Renamed RP journal entry.")
end

function UI:ConfirmDeleteJournalEntry(index)
  index = index or self.selectedJournalIndex
  local entry = self:Editor().journal[index]
  if not entry then
    self:SetStatus("Select a journal entry to delete.")
    return
  end

  self.selectedJournalIndex = index
  self:RefreshHistory()
  ensure_journal_delete_dialog()
  if StaticPopup_Show then
    StaticPopup_Show("LONGCHAT_JOURNAL_DELETE", entry.title or "Event", nil, {
      ui = self,
      index = index,
      title = entry.title or "Event",
    })
  end
end

function UI:DeleteJournalEntry(index)
  local journal = self:Editor().journal
  if not journal[index] then
    return
  end

  table.remove(journal, index)
  if self.selectedJournalIndex then
    if #journal == 0 then
      self.selectedJournalIndex = nil
    elseif self.selectedJournalIndex >= index then
      self.selectedJournalIndex = math.min(self.selectedJournalIndex, #journal)
    end
  end
  self:RefreshHistory()
  self:SetStatus("Deleted RP journal entry.")
end

function UI:ColorJournalText(hex)
  local editbox = self.notesBox
  if not editbox or not hex then
    return
  end

  local text = editbox:GetText() or ""
  local selectedLeft, selectedRight = editbox_selection(editbox)
  if not selectedLeft and self.notesSelectionStart and self.notesSelectionEnd and self.notesSelectionStart ~= self.notesSelectionEnd then
    local left = math.min(self.notesSelectionStart, self.notesSelectionEnd)
    local right = math.max(self.notesSelectionStart, self.notesSelectionEnd)
    selectedLeft = left + 1
    selectedRight = right
  end

  if selectedLeft and selectedRight then
    local selected = plain_color_text(text:sub(selectedLeft, selectedRight))
    local replacement = "|cff" .. hex .. selected .. "|r"
    editbox:SetText(text:sub(1, selectedLeft - 1) .. replacement .. text:sub(selectedRight + 1))
    editbox:SetCursorPosition(selectedLeft - 1 + #replacement)
    self.notesSelectionStart = nil
    self.notesSelectionEnd = nil
    editbox:SetFocus()
    self:RememberNotes()
    return
  end

  local cursor = editbox:GetCursorPosition() or 0
  local left = cursor + 1
  local right = cursor

  while left > 1 and text:sub(left - 1, left - 1):match("%S") do
    left = left - 1
  end

  while right < #text and text:sub(right + 1, right + 1):match("%S") do
    right = right + 1
  end

  if right >= left then
    local word = plain_color_text(text:sub(left, right))
    local replacement = "|cff" .. hex .. word .. "|r"
    editbox:SetText(text:sub(1, left - 1) .. replacement .. text:sub(right + 1))
    editbox:SetCursorPosition(left - 1 + #replacement)
  else
    local sample = "|cff" .. hex .. "colored text|r"
    editbox:SetText(text:sub(1, cursor) .. sample .. text:sub(cursor + 1))
    editbox:SetCursorPosition(cursor + #sample)
  end

  editbox:SetFocus()
  self:RememberNotes()
end

function UI:ClearJournalColors()
  if not self.notesBox then
    return
  end

  local cursor = self.notesBox:GetCursorPosition() or 0
  self.notesBox:SetText(plain_color_text(self.notesBox:GetText() or ""))
  self.notesBox:SetCursorPosition(math.min(cursor, #(self.notesBox:GetText() or "")))
  self.notesBox:SetFocus()
  self:RememberNotes()
  self:SetStatus("Removed journal color formatting.")
end

function UI:SendComposer()
  local channel = self.channelDropdown and self.channelDropdown:GetValue() or self:Editor().channel or "SAY"
  if not self.editBox then
    return
  end

  if not self:CanUseChannel(channel) then
    local message = "Cannot send to " .. self:LabelFor(channel) .. " right now."
    LC.SafePrint(message)
    self:SetStatus(message)
    return
  end

  local outgoing = self.editBox:GetText() or ""
  local spell = self:Spell()
  if spell then
    outgoing = spell:RemoveHighlighting(outgoing)
  end

  local ok, err = LC.SendMessage(outgoing, channel)
  if not ok then
    LC.SafePrint(err or "Unable to send message.")
    self:SetStatus(err or "Unable to send message.")
    return
  end

  self:RecordHistory(outgoing, channel)
  self:RefreshHistory()

  if self:Editor().clearOnSend then
    self.editBox:SetText("")
  else
    self.editBox:SetText(outgoing)
  end
  self:RememberDraft()
  self:RefreshCharacterCounter()
  self.editBox:SetFocus()
  self:SetStatus("Sent via " .. self:LabelFor(channel) .. ".")
end

function UI:ResyncComposerCursor()
  local editbox = self.editBox
  if not editbox then
    return
  end

  local text = editbox:GetText() or ""
  local spell = self:Spell()
  if spell and spell.RemoveHighlighting then
    text = spell:RemoveHighlighting(text)
  end

  self.formattingComposerDraft = true
  editbox:SetText(text)
  self.formattingComposerDraft = false

  self:ApplyComposerSplitMarkers(editbox, true)
  local current = editbox:GetText() or ""
  if editbox.SetCursorPosition then
    editbox:SetCursorPosition(#current)
  end
  editbox:SetFocus()
  self:RememberDraft()
  self:RefreshCharacterCounter()
  self:RefreshComposerSpellcheckMode()
  self:SetStatus("Composer cursor resynced.")
end

function UI:FocusComposerEditBox()
  local editbox = self.editBox
  if not editbox then
    return
  end

  editbox:SetFocus()
  if editbox.SetCursorPosition then
    editbox:SetCursorPosition(#(editbox:GetText() or ""))
  end
end

function UI:ScheduleComposerSoftCursorResync(editbox)
  editbox = editbox or self.editBox
  if not editbox or self:Editor().composerSpellDisplay ~= "inline" then
    return
  end
  if not C_Timer or not C_Timer.After or not editbox.GetCursorPosition or not editbox.SetCursorPosition then
    return
  end

  self.softCursorResyncToken = (self.softCursorResyncToken or 0) + 1
  local token = self.softCursorResyncToken
  local position = editbox:GetCursorPosition() or 0
  C_Timer.After(0, function()
    if token ~= UI.softCursorResyncToken then
      return
    end
    if not editbox or not editbox.GetCursorPosition or not editbox.SetCursorPosition then
      return
    end
    if editbox:GetCursorPosition() == position then
      editbox:SetCursorPosition(position)
    end
  end)
end

function UI:ApplyComposerSplitMarkers(editbox, force)
  if not editbox or self.formattingComposerDraft then
    return editbox and editbox:GetText() or ""
  end

  local current = editbox:GetText() or ""
  local cursor = editbox.GetCursorPosition and editbox:GetCursorPosition() or 0
  local cleaned = current
  local cleanedCursor = cursor
  local spell = self:Spell()
  if spell and spell.RemoveHighlighting then
    cleaned, cleanedCursor = spell:RemoveHighlighting(current, cursor)
  end

  local channel = self.channelDropdown and self.channelDropdown:GetValue() or self:Editor().channel or "SAY"
  local afterCursor = cleaned:sub((cleanedCursor or 0) + 1, (cleanedCursor or 0) + 8)
  local beforeSplitMarker = afterCursor:match("^%s*>>") ~= nil
  if not force and (cleanedCursor or 0) < #cleaned and not beforeSplitMarker then
    return current
  end

  local formatted, formattedCursor
  if LC.FormatComposerDraftWithCursor then
    formatted, formattedCursor = LC.FormatComposerDraftWithCursor(cleaned, channel, cleanedCursor)
  else
    formatted = LC.FormatComposerDraft and LC.FormatComposerDraft(cleaned, channel) or cleaned
    formattedCursor = math.min(cleanedCursor or #formatted, #formatted)
  end
  if formatted == cleaned then
    return current
  end

  self.formattingComposerDraft = true
  editbox:SetText(formatted)
  if editbox.SetCursorPosition then
    editbox:SetCursorPosition(force and math.min(formattedCursor or #formatted, #formatted) or #formatted)
  end
  self.formattingComposerDraft = false
  return formatted
end

function UI:RefreshCharacterCounter()
  if not self.characterCounter then
    return
  end
  local text = self.editBox and self.editBox:GetText() or ""
  local clean = LC.GetComposerDraftText and LC.GetComposerDraftText(text) or text
  local chunks = LC.GetSplitPreview and LC.GetSplitPreview(clean, self:Editor().channel) or {}
  local chunkCount = math.max(1, #chunks)
  self.characterCounter:SetText(("%d chars | %d chunk%s"):format(#clean, chunkCount, chunkCount == 1 and "" or "s"))
end

function UI:RefreshSpellcheckOverlay()
  if not self.spellOverlay or not self.editBox then
    return
  end

  local spell = LC.Spellcheck
  if self:Editor().composerSpellDisplay ~= "footer"
    or not spell
    or not spell.CheckLine
    or not spell.GetFlaggedWords
    or not spell.IsAvailable
    or not spell:IsAvailable() then
    self.spellOverlay:SetText("")
    return
  end

  local text = self.editBox:GetText() or ""
  local clean = spell.RemoveHighlighting and spell:RemoveHighlighting(text) or text
  spell:CheckLine(clean, self.editBox)
  local words = spell:GetFlaggedWords(self.editBox)
  if not words or #words == 0 then
    self.spellOverlay:SetText("")
    return
  end

  local unique = {}
  local samples = {}
  for _, word in ipairs(words) do
    local text = tostring(word.Word or word.word or "")
    if text == "" then
      local startPos = tonumber(word.RawStartPos or word.StartPos)
      local endPos = tonumber(word.RawEndPos or word.EndPos)
      if startPos and endPos and endPos >= startPos then
        text = clean:sub(startPos, endPos)
      end
    end
    text = text:gsub("|", "")
    if text ~= "" and not unique[text] then
      unique[text] = true
      samples[#samples + 1] = text
      if #samples >= 3 then
        break
      end
    end
  end

  local sampleText = #samples > 0 and (" - " .. table.concat(samples, ", ")) or ""
  self.spellOverlay:SetText(("Spelling: %d issue%s%s"):format(#words, #words == 1 and "" or "s", sampleText))
end

function UI:RefreshComposerSpellcheckMode()
  if not self.editBox then
    return
  end

  local display = self:Editor().composerSpellDisplay or "footer"
  self.editBox._LongChatSpellcheckNoInline = display ~= "inline"
  if self.spellOverlay then
    self.spellOverlay:SetText("")
  end
  if LC.Spellcheck and LC.Spellcheck.RemoveHighlighting then
    local text = self.editBox:GetText() or ""
    local cursor = self.editBox.GetCursorPosition and self.editBox:GetCursorPosition() or 0
    local cleaned, cleanedCursor = LC.Spellcheck:RemoveHighlighting(text, cursor)
    if display ~= "inline" and cleaned ~= text then
      self.formattingComposerDraft = true
      self.editBox:SetText(cleaned)
      if self.editBox.SetCursorPosition then
        self.editBox:SetCursorPosition(cleanedCursor or math.min(cursor, #cleaned))
      end
      self.formattingComposerDraft = false
    end
  end
  if display == "inline" then
    LC.Spellcheck:SpellCheckChat(self.editBox)
    self:ScheduleComposerSoftCursorResync(self.editBox)
  end
  self:RefreshSpellcheckOverlay()
end

function UI:SetTab(tab)
  tab = tab == TAB_NOTES and TAB_NOTES or TAB_COMPOSE
  local editor = self:Editor()
  editor.activeTab = tab

  if self.composePage then
    self.composePage:SetShown(tab == TAB_COMPOSE)
  end
  if self.notesPage then
    self.notesPage:SetShown(tab == TAB_NOTES)
  end

  if self.composeTab and self.notesTab then
    self.composeTab:SetEnabled(tab ~= TAB_COMPOSE)
    self.notesTab:SetEnabled(tab ~= TAB_NOTES)
  end

  if tab == TAB_NOTES then
    self:RefreshHistory()
    if self.notesBox then
      self.notesBox:SetFocus()
    end
  elseif self.editBox then
    self.editBox:SetFocus()
  end
end

function UI:BuildRail(parent)
  local rail = CreateFrame("Frame", nil, parent, "BackdropTemplate")
  rail:SetPoint("TOPLEFT", parent, "TOPLEFT", 14, -14)
  rail:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 14, 14)
  rail:SetWidth(126)
  rail:SetBackdrop(panel_backdrop())
  tint(rail, 0.018, 0.025, 0.032, 0.82)

  local title = put_label(rail, "LongChat", "GameFontNormalLarge")
  title:SetPoint("TOPLEFT", rail, "TOPLEFT", 12, -14)

  local compose = self:Button(rail, "Compose", 100, function()
    UI:SetTab(TAB_COMPOSE)
  end)
  compose:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -22)

  local notes = self:Button(rail, "RP Notes", 100, function()
    UI:SetTab(TAB_NOTES)
  end)
  notes:SetPoint("TOPLEFT", compose, "BOTTOMLEFT", 0, -8)

  local spell = put_label(rail, self:SpellStatus(), "GameFontHighlightSmall")
  spell:SetPoint("BOTTOMLEFT", rail, "BOTTOMLEFT", 12, 12)
  spell:SetPoint("RIGHT", rail, "RIGHT", -10, 0)
  spell:SetJustifyH("LEFT")

  self.composeTab = compose
  self.notesTab = notes
  self.spellLabel = spell
end

function UI:BuildComposePage(parent)
  local page = CreateFrame("Frame", nil, parent)
  page:SetPoint("TOPLEFT", parent, "TOPLEFT", 154, -14)
  page:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -14, 14)

  local header = CreateFrame("Frame", nil, page, "BackdropTemplate")
  header:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0)
  header:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, 0)
  header:SetHeight(54)
  header:SetBackdrop(panel_backdrop())
  tint(header, 0.035, 0.045, 0.055, 0.72)

  local prompt = put_label(header, "Draft", "GameFontNormal")
  prompt:SetPoint("LEFT", header, "LEFT", 12, 0)

  local menu = self:CreateChannelMenu(header)
  menu.frame:SetPoint("LEFT", prompt, "RIGHT", -8, -2)

  local send = self:Button(header, "Send", 72, function()
    UI:SendComposer()
  end)
  send:SetPoint("RIGHT", header, "RIGHT", -10, 0)

  local load = self:Button(header, "Load", 62, function()
    UI:LoadActiveChat()
  end)
  load:SetPoint("RIGHT", send, "LEFT", -8, 0)

  local clear = self:Button(header, "Clear", 62, function()
    UI:ClearComposer()
  end)
  clear:SetPoint("RIGHT", load, "LEFT", -8, 0)

  local writingPad = CreateFrame("Frame", nil, page, "BackdropTemplate")
  writingPad:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -10)
  writingPad:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", -54, 44)
  writingPad:SetBackdrop(panel_backdrop())
  tint(writingPad, 0.012, 0.016, 0.020, 0.84)
  writingPad:EnableMouse(true)
  writingPad:SetScript("OnMouseDown", function()
    UI:FocusComposerEditBox()
  end)

  local scroll = CreateFrame("ScrollFrame", "LongChatComposerScrollFrame", writingPad, "UIPanelScrollFrameTemplate")
  scroll:SetPoint("TOPLEFT", writingPad, "TOPLEFT", 10, -10)
  scroll:SetPoint("BOTTOMRIGHT", writingPad, "BOTTOMRIGHT", -28, 10)
  scroll:EnableMouse(true)
  scroll:SetScript("OnMouseDown", function()
    UI:FocusComposerEditBox()
  end)

  local editbox = CreateFrame("EditBox", "LongChatComposerEdit", scroll)
  editbox:SetMultiLine(true)
  editbox:SetAutoFocus(false)
  editbox:SetFontObject(ChatFontNormal)
  editbox:SetWidth(360)
  editbox:SetMaxLetters(0 + 0)
  editbox:SetTextInsets(4, 4, 4, 4)
  editbox:SetText(self:Editor().draft or "")
  editbox:SetScript("OnTextChanged", function(self)
    local raw = self:GetText() or ""
    local clean = LC.GetComposerDraftText and LC.GetComposerDraftText(raw) or raw
    local previousClean = UI.lastComposerCleanText or clean
    local forceMarkers = math.abs(#clean - #previousClean) > 1
    local text = UI:ApplyComposerSplitMarkers(self, forceMarkers)
    UI.lastComposerCleanText = LC.GetComposerDraftText and LC.GetComposerDraftText(text or "") or (text or "")
    UI:Editor().draft = text or ""
    UI:RefreshCharacterCounter()
    UI:RefreshSpellcheckOverlay()
    if ScrollingEdit_OnTextChanged then
      ScrollingEdit_OnTextChanged(self, scroll)
    end
  end)
  editbox:SetScript("OnCursorChanged", function(self, x, y, w, h)
    if UI:Editor().composerSpellDisplay == "inline" and y ~= UI.lastComposerCursorY then
      UI.lastComposerCursorY = y
      UI:ScheduleComposerSoftCursorResync(self)
    end
    if ScrollingEdit_OnCursorChanged then
      ScrollingEdit_OnCursorChanged(self, x, y, w, h)
    end
  end)
  editbox:SetScript("OnEscapePressed", function(self)
    self:ClearFocus()
  end)
  editbox._LongChatComposer = true
  editbox.editBox = editbox
  scroll:SetScrollChild(editbox)

  writingPad:SetScript("OnSizeChanged", function(self)
    editbox:SetWidth(math.max(260, self:GetWidth() - 42))
    UI:RefreshSpellcheckOverlay()
  end)

  self:BuildMarkerDock(writingPad)

  local clearCheck = CreateFrame("CheckButton", "LongChatComposerClearAfterSend", page, "InterfaceOptionsCheckButtonTemplate")
  clearCheck:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 0, 10)
  _G[clearCheck:GetName() .. "Text"]:SetText("Clear after send")
  clearCheck:SetChecked(self:Editor().clearOnSend)
  clearCheck:SetScript("OnClick", function(self)
    UI:Store("clearOnSend", self:GetChecked() == true)
  end)

  local resync = self:Button(page, "Resync Cursor", 112, function()
    UI:ResyncComposerCursor()
  end)
  resync:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 170, 10)

  local counter = put_label(page, "", "GameFontDisableSmall")
  counter:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", -54, 17)
  counter:SetJustifyH("RIGHT")

  local spellOverlay = put_label(page, "", "GameFontDisableSmall")
  spellOverlay:SetPoint("BOTTOMRIGHT", counter, "TOPRIGHT", 0, 2)
  spellOverlay:SetJustifyH("RIGHT")
  spellOverlay:SetWidth(320)
  spellOverlay:SetTextColor(0.49, 0.78, 0.98, 1)

  self.composePage = page
  self.editBox = editbox
  self.composerClearCheck = clearCheck
  self.resyncCursorButton = resync
  self.scrollFrame = scroll
  self.spellOverlay = spellOverlay
  self.characterCounter = counter
  self:RefreshTextColor()
  self:RefreshComposerFontScale()
  self:ApplyComposerSplitMarkers(editbox, true)
  self.lastComposerCleanText = LC.GetComposerDraftText and LC.GetComposerDraftText(editbox:GetText() or "") or (editbox:GetText() or "")
  self:RefreshCharacterCounter()
  self:RefreshSpellcheckOverlay()
end

function UI:BuildNotesPage(parent)
  local page = CreateFrame("Frame", nil, parent)
  page:SetPoint("TOPLEFT", parent, "TOPLEFT", 154, -14)
  page:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -14, 14)

  local notesCard = CreateFrame("Frame", nil, page, "BackdropTemplate")
  notesCard:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0)
  notesCard:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, 0)
  notesCard:SetHeight(190)
  notesCard:SetBackdrop(panel_backdrop())
  tint(notesCard, 0.035, 0.045, 0.052, 0.76)

  local notesTitle = put_label(notesCard, "Current Note", "GameFontNormal")
  notesTitle:SetPoint("TOPLEFT", notesCard, "TOPLEFT", 12, -10)

  local saveNote = self:Button(notesCard, "Save Note", 86, function()
    UI:SaveJournal()
  end)
  saveNote:SetPoint("TOPRIGHT", notesCard, "TOPRIGHT", -86, -8)

  local newNote = self:Button(notesCard, "New", 62, function()
    UI:NewJournalNote()
  end)
  newNote:SetPoint("LEFT", saveNote, "RIGHT", 8, 0)

  local colorBar = CreateFrame("Frame", nil, notesCard)
  colorBar:SetPoint("TOPLEFT", notesTitle, "BOTTOMLEFT", 0, -8)
  colorBar:SetSize(240, 20)

  local colorLabel = put_label(colorBar, "Color", "GameFontDisableSmall")
  colorLabel:SetPoint("LEFT", colorBar, "LEFT", 0, 0)

  local previous = colorLabel
  for index, color in ipairs(JOURNAL_COLORS) do
    local swatch = CreateFrame("Button", nil, colorBar, "BackdropTemplate")
    swatch:SetSize(16, 16)
    swatch:SetBackdrop(panel_backdrop())
    swatch:SetBackdropColor(color.r, color.g, color.b, 1)
    swatch:SetBackdropBorderColor(0.15, 0.15, 0.15, 1)
    swatch:SetPoint("LEFT", previous, "RIGHT", index == 1 and 8 or 4, 0)
    swatch:SetScript("OnClick", function()
      UI:ColorJournalText(color.hex)
    end)
    swatch:SetScript("OnEnter", function(self)
      self:SetBackdropBorderColor(1, 0.82, 0.28, 1)
    end)
    swatch:SetScript("OnLeave", function(self)
      self:SetBackdropBorderColor(0.15, 0.15, 0.15, 1)
    end)
    previous = swatch
  end

  local plain = self:Button(colorBar, "Plain", 50, function()
    UI:ClearJournalColors()
  end)
  plain:SetPoint("LEFT", previous, "RIGHT", 8, 0)

  local notesScroll = CreateFrame("ScrollFrame", "LongChatNotesScrollFrame", notesCard, "UIPanelScrollFrameTemplate")
  notesScroll:SetPoint("TOPLEFT", notesCard, "TOPLEFT", 10, -58)
  notesScroll:SetPoint("BOTTOMRIGHT", notesCard, "BOTTOMRIGHT", -28, 10)

  local notesBox = CreateFrame("EditBox", "LongChatNotesEdit", notesScroll)
  notesBox:SetMultiLine(true)
  notesBox:SetAutoFocus(false)
  notesBox:SetFontObject(ChatFontNormal)
  notesBox:SetWidth(360)
  notesBox:SetMaxLetters(0 + 0)
  notesBox:SetTextInsets(4, 4, 4, 4)
  notesBox:SetText(self:Editor().notes or "")
  notesBox:SetScript("OnTextChanged", function(self)
    UI:Editor().notes = self:GetText() or ""
    if ScrollingEdit_OnTextChanged then
      ScrollingEdit_OnTextChanged(self, notesScroll)
    end
  end)
  notesBox:SetScript("OnCursorChanged", function(self, x, y, w, h)
    if ScrollingEdit_OnCursorChanged then
      ScrollingEdit_OnCursorChanged(self, x, y, w, h)
    end
  end)
  notesBox:SetScript("OnEscapePressed", function(self)
    self:ClearFocus()
  end)
  notesBox:SetScript("OnMouseDown", function(self)
    UI.notesSelectionStart = self:GetCursorPosition() or 0
    UI.notesSelectionEnd = UI.notesSelectionStart
  end)
  notesBox:SetScript("OnMouseUp", function(self)
    UI.notesSelectionEnd = self:GetCursorPosition() or UI.notesSelectionStart
  end)
  notesScroll:SetScrollChild(notesBox)
  notesCard:SetScript("OnSizeChanged", function(self)
    notesBox:SetWidth(math.max(260, self:GetWidth() - 42))
  end)

  local journalCard = CreateFrame("Frame", nil, page, "BackdropTemplate")
  journalCard:SetPoint("TOPLEFT", notesCard, "BOTTOMLEFT", 0, -10)
  journalCard:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 44)
  journalCard:SetBackdrop(panel_backdrop())
  tint(journalCard, 0.012, 0.016, 0.020, 0.84)

  local historyTitle = put_label(journalCard, "RP Journal", "GameFontNormal")
  historyTitle:SetPoint("TOPLEFT", journalCard, "TOPLEFT", 12, -10)

  local deleteEntry = self:Button(journalCard, "Delete", 62, function()
    UI:ConfirmDeleteJournalEntry(UI.selectedJournalIndex)
  end)
  deleteEntry:SetPoint("TOPRIGHT", journalCard, "TOPRIGHT", -12, -8)

  local renameEntry = self:Button(journalCard, "Rename", 72, function()
    UI:PromptRenameJournalEntry(UI.selectedJournalIndex)
  end)
  renameEntry:SetPoint("RIGHT", deleteEntry, "LEFT", -8, 0)

  local journalHint = put_label(journalCard, "Select an entry to rename or delete", "GameFontDisableSmall")
  journalHint:SetPoint("LEFT", historyTitle, "RIGHT", 12, 0)
  journalHint:SetPoint("RIGHT", renameEntry, "LEFT", -12, 0)
  journalHint:SetJustifyH("LEFT")

  local historyScroll = CreateFrame("ScrollFrame", "LongChatHistoryScrollFrame", journalCard, "UIPanelScrollFrameTemplate")
  historyScroll:SetPoint("TOPLEFT", journalCard, "TOPLEFT", 12, -38)
  historyScroll:SetPoint("BOTTOMRIGHT", journalCard, "BOTTOMRIGHT", -28, 10)

  local historyBody = CreateFrame("Frame", nil, historyScroll)
  historyBody:SetSize(360, 400)
  historyScroll:SetScrollChild(historyBody)

  local historyText = put_label(historyBody, "", "GameFontHighlightSmall")
  historyText:SetPoint("TOPLEFT", historyBody, "TOPLEFT", 0, 0)
  historyText:SetPoint("TOPRIGHT", historyBody, "TOPRIGHT", 0, 0)
  historyText:SetJustifyH("LEFT")
  historyText:SetJustifyV("TOP")

  journalCard:SetScript("OnSizeChanged", function(self)
    local width = math.max(260, self:GetWidth() - 48)
    historyBody:SetWidth(width)
    historyText:SetWidth(width)
    UI:RefreshHistory()
  end)

  self.notesPage = page
  self.notesBox = notesBox
  self.historyText = historyText
  self.historyBody = historyBody
  self:RefreshTextColor()
  self:RefreshComposerFontScale()
end

function UI:CreateComposer()
  if self.frame then
    return self.frame
  end

  local editor = self:Editor()
  local frame = CreateFrame("Frame", "LongChatComposerFrame", UIParent, "BackdropTemplate")
  frame:SetSize(editor.size.width or self.COMPOSER_WIDTH, editor.size.height or self.COMPOSER_HEIGHT)
  frame:SetPoint(editor.position.point, UIParent, editor.position.point, editor.position.x, editor.position.y)
  frame:SetFrameStrata("DIALOG")
  frame:SetMovable(true)
  frame:EnableMouse(true)
  frame:RegisterForDrag("LeftButton")
  frame:SetClampedToScreen(true)
  frame:SetBackdrop({
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    tile = false,
    edgeSize = 14,
    insets = { left = 3, right = 3, top = 3, bottom = 3 },
  })
  tint(frame, 0.006, 0.009, 0.013, 0.86)
  frame:SetScript("OnDragStart", frame.StartMoving)
  frame:SetScript("OnDragStop", function(self)
    self:StopMovingOrSizing()
    UI:RememberPosition(self)
  end)
  frame:SetScript("OnHide", function(self)
    UI:RememberPosition(self)
    UI:RememberDraft()
    UI:RememberNotes()
  end)
  frame.frame = frame

  local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
  close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -5, -5)

  self:BuildRail(frame)
  self:BuildComposePage(frame)
  self:BuildNotesPage(frame)

  local status = frame:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
  status:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 154, 48)
  status:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -72, 48)
  status:SetJustifyH("LEFT")
  status:SetText("")

  function frame:SetStatusText(text)
    status:SetText(text or "")
  end

  self.frame = frame
  self.statusText = status

  self:AttachSpellcheck(self.editBox)
  self:ApplyScale()
  self:SetTab(editor.activeTab)
  frame:Hide()
  return frame
end

function LC.ShowComposer()
  if UI:IsRestricted() then
    LC.SafePrint("Composer disabled during combat.")
    return
  end

  local frame = UI:CreateComposer()
  if UI.spellLabel then
    UI.spellLabel:SetText(UI:SpellStatus())
  end
  if UI.composerClearCheck then
    UI.composerClearCheck:SetChecked(UI:Editor().clearOnSend)
  end
  UI:SetChannel(UI:Editor().channel)
  UI:ApplyScale()
  UI:AttachSpellcheck(UI.editBox)
  UI:RefreshComposerSpellcheckMode()
  UI:RefreshHistory()
  UI:SetStatus("")
  frame:Show()
  UI:SetTab(UI:Editor().activeTab)
end

function LC.HideComposer()
  if UI.frame then
    UI.frame:Hide()
  end
end

function LC.ToggleComposer()
  if UI.frame and UI.frame:IsShown() then
    LC.HideComposer()
  else
    LC.ShowComposer()
  end
end

local guard = CreateFrame("Frame")
guard:RegisterEvent("PLAYER_REGEN_DISABLED")
guard:RegisterEvent("PLAYER_ENTERING_WORLD")
guard:RegisterEvent("ZONE_CHANGED_NEW_AREA")
guard:SetScript("OnEvent", function()
  if UI.frame and UI.frame:IsShown() and UI:IsRestricted() then
    LC.HideComposer()
    LC.SafePrint("Composer disabled during combat.")
  end
end)
