local addon_name, ns = ...

local shared = _G.KagrokSharedLauncher
if type(shared) ~= "table" then
  shared = {}
  _G.KagrokSharedLauncher = shared
end
ns.KagrokSharedLauncher = shared

local CreateFrame = CreateFrame
local GameTooltip = GameTooltip
local GetCursorPosition = GetCursorPosition
local Minimap = Minimap
local UIParent = UIParent
local Settings = Settings
local C_Timer = C_Timer
local tostring = tostring
local tonumber = tonumber
local ipairs = ipairs
local pairs = pairs
local math = math
local pcall = pcall
local type = type
local table_insert = table.insert
local table_sort = table.sort

local MODULE_VERSION = 1
local DEFAULT_SHARED_ICON = "Interface\\Icons\\INV_Misc_QuestionMark"
local DEFAULT_BUTTON_ICON = "Interface\\Icons\\INV_Misc_QuestionMark"
local MIN_SCROLLBAR_ENTRY_COUNT = 5
local MENU_WIDTH = 236
local MENU_MAX_HEIGHT = 420
local MENU_ROW_HEIGHT = 24

local function trim_text(value)
  local text = tostring(value or "")
  text = text:gsub("^%s+", "")
  text = text:gsub("%s+$", "")
  return text
end

local function ensure_db()
  if type(_G.KagrokLauncherDB) ~= "table" then
    _G.KagrokLauncherDB = {}
  end
  local db = _G.KagrokLauncherDB
  db.angle = tonumber(db.angle) or 225
  db.settings = db.settings or {}
  db.settings.hide_stack = db.settings.hide_stack == true
  db.settings.priority_overrides = db.settings.priority_overrides or {}
  db.settings.hidden_addons = db.settings.hidden_addons or {}
  db.settings.left_click_addon = trim_text(db.settings.left_click_addon)
  db.settings.tooltip_addon = trim_text(db.settings.tooltip_addon)
  return db
end

local function get_addon_metadata(addon_key, field)
  local value = ""
  if type(GetAddOnMetadata) == "function" then
    value = tostring(GetAddOnMetadata(addon_key, field) or "")
  end
  if value == "" and C_AddOns and type(C_AddOns.GetAddOnMetadata) == "function" then
    value = tostring(C_AddOns.GetAddOnMetadata(addon_key, field) or "")
  end
  return trim_text(value)
end

local function safe_call(callback, ...)
  if type(callback) ~= "function" then
    return nil
  end
  local ok, result = pcall(callback, ...)
  if ok then
    return result
  end
  return nil
end

local function get_polar_angle_degrees(dy, dx)
  if type(math.atan2) == "function" then
    return math.deg(math.atan2(dy, dx))
  end
  if dx == 0 then
    if dy > 0 then
      return 90
    elseif dy < 0 then
      return 270
    end
    return 0
  end
  local angle = math.deg(math.atan(dy / dx))
  if dx < 0 then
    angle = angle + 180
  elseif dy < 0 then
    angle = angle + 360
  end
  return angle
end

local function create_backdrop(frame, bg, border)
  if not frame or not frame.SetBackdrop then
    return
  end
  frame:SetBackdrop({
    bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    tile = true,
    tileSize = 16,
    edgeSize = 14,
    insets = { left = 3, right = 3, top = 3, bottom = 3 },
  })
  frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1)
  frame:SetBackdropBorderColor(border[1], border[2], border[3], border[4] or 1)
end

local function create_frame_with_optional_template(frame_type, name, parent, template)
  if template and template ~= "" then
    local ok, frame = pcall(CreateFrame, frame_type, name, parent, template)
    if ok and frame then
      return frame
    end
  end
  return CreateFrame(frame_type, name, parent)
end

local function get_effective_priority(entry)
  local db = ensure_db()
  local overrides = db.settings and db.settings.priority_overrides
  local override = overrides and tonumber(overrides[entry.id])
  if override then
    return override
  end
  return tonumber(entry.priority) or 0
end

local function sort_entries(a, b)
  local ap = get_effective_priority(a)
  local bp = get_effective_priority(b)
  if ap ~= bp then
    return ap > bp
  end
  return tostring(a.name or a.id or "") < tostring(b.name or b.id or "")
end

local function normalize_entry(definition)
  local addon_key = trim_text(definition.addon_name or definition.id or addon_name)
  local id = trim_text(definition.id or addon_key)
  local name = trim_text(definition.name)
  if name == "" then
    name = get_addon_metadata(addon_key, "Title")
  end
  if name == "" then
    name = id
  end

  local priority = tonumber(definition.priority)
  if not priority then
    priority = tonumber(get_addon_metadata(addon_key, "X-KagrokLauncherPriority")) or 0
  end

  local icon = trim_text(definition.icon)
  if icon == "" then
    icon = get_addon_metadata(addon_key, "IconTexture")
  end
  if icon == "" then
    icon = DEFAULT_BUTTON_ICON
  end

  local shared_icon = trim_text(definition.shared_icon or definition.family_icon)
  if shared_icon == "" then
    shared_icon = DEFAULT_SHARED_ICON
  end

  return {
    id = id,
    addon_name = addon_key,
    name = name,
    priority = priority,
    icon = icon,
    shared_icon = shared_icon,
    is_enabled = definition.is_enabled,
    left_click = definition.left_click,
    tooltip = definition.tooltip,
    menu_items = definition.menu_items,
    shared_items = definition.shared_items,
  }
end

local function normalize_settings_entry(definition)
  local addon_key = trim_text(definition.addon_name or definition.id or addon_name)
  local id = trim_text(definition.id or addon_key)
  local name = trim_text(definition.name)
  if name == "" then
    name = get_addon_metadata(addon_key, "Title")
  end
  if name == "" then
    name = id
  end

  local priority = tonumber(definition.priority)
  if not priority then
    priority = tonumber(get_addon_metadata(addon_key, "X-KagrokLauncherPriority")) or 0
  end

  local icon = trim_text(definition.icon)
  if icon == "" then
    icon = get_addon_metadata(addon_key, "IconTexture")
  end
  if icon == "" then
    icon = DEFAULT_BUTTON_ICON
  end

  local get_panel = definition.get_panel
  if type(get_panel) ~= "function" and definition.panel then
    get_panel = function()
      return definition.panel
    end
  end

  return {
    id = id,
    addon_name = addon_key,
    name = name,
    priority = priority,
    icon = icon,
    get_panel = get_panel,
    is_enabled = definition.is_enabled,
    on_registered = definition.on_registered,
  }
end

function shared:RegisterAddon(definition)
  if type(definition) ~= "table" then
    return nil
  end
  self.registry = self.registry or {}
  local entry = normalize_entry(definition)
  self.registry[entry.id] = entry
  self:Refresh()
  return entry
end

function shared:UnregisterAddon(addon_id)
  self.registry = self.registry or {}
  self.registry[tostring(addon_id or "")] = nil
  self:Refresh()
end

function shared:RegisterSettingsPage(definition)
  if type(definition) ~= "table" then
    return nil
  end
  self.settings_registry = self.settings_registry or {}
  local entry = normalize_settings_entry(definition)
  if type(entry.get_panel) ~= "function" then
    return nil
  end
  self.settings_registry[entry.id] = entry
  self:EnsureSettingsBootstrap()
  if type(IsLoggedIn) == "function" and IsLoggedIn() then
    self:ScheduleSettingsFinalize()
  end
  return entry
end

function shared:UnregisterSettingsPage(addon_id)
  self.settings_registry = self.settings_registry or {}
  self.settings_registry[tostring(addon_id or "")] = nil
end

function shared:GetEntries(include_disabled)
  self.registry = self.registry or {}
  local db = ensure_db()
  local hidden_addons = db.settings and db.settings.hidden_addons or {}
  local entries = {}
  for _, entry in pairs(self.registry) do
    local enabled = true
    if type(entry.is_enabled) == "function" then
      enabled = safe_call(entry.is_enabled) ~= false
    end
    if hidden_addons[entry.id] == true then
      enabled = false
    end
    if include_disabled or enabled then
      table_insert(entries, entry)
    end
  end
  table_sort(entries, sort_entries)
  return entries
end

function shared:GetSettingsEntries(include_disabled)
  self.settings_registry = self.settings_registry or {}
  local entries = {}
  for _, entry in pairs(self.settings_registry) do
    local enabled = true
    if type(entry.is_enabled) == "function" then
      enabled = safe_call(entry.is_enabled) ~= false
    end
    if include_disabled or enabled then
      table_insert(entries, entry)
    end
  end
  table_sort(entries, sort_entries)
  return entries
end

function shared:GetPrimaryEntry()
  local entries = self:GetEntries(false)
  return entries[1], entries
end

function shared:GetSharedIcon(entries)
  local active_entries = entries or self:GetEntries(false)
  for _, entry in ipairs(active_entries) do
    local icon = trim_text(entry.shared_icon)
    if icon ~= "" then
      return icon
    end
  end
  return DEFAULT_SHARED_ICON
end

function shared:EnsureButton()
  if self.button then
    return self.button
  end
  if type(CreateFrame) ~= "function" or not Minimap then
    return nil
  end

  local button = CreateFrame("Button", "KagrokSharedLauncherMinimapButton", Minimap)
  button:SetSize(32, 32)
  button:SetFrameStrata("MEDIUM")
  button:SetFrameLevel((Minimap:GetFrameLevel() or 1) + 8)
  button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
  button:RegisterForDrag("LeftButton")
  button:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")

  local background = button:CreateTexture(nil, "BACKGROUND")
  background:SetSize(20, 20)
  background:SetPoint("CENTER")
  background:SetTexture("Interface\\Minimap\\UI-Minimap-Background")
  background:SetVertexColor(0, 0, 0, 0.55)

  local icon = button:CreateTexture(nil, "ARTWORK")
  icon:SetSize(20, 20)
  icon:SetPoint("CENTER")
  icon:SetTexture(DEFAULT_BUTTON_ICON)
  icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

  local border = button:CreateTexture(nil, "OVERLAY")
  border:SetSize(53, 53)
  border:SetPoint("TOPLEFT")
  border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")

  local pushed = button:CreateTexture(nil, "ARTWORK")
  pushed:SetSize(20, 20)
  pushed:SetPoint("CENTER", 1, -1)
  pushed:SetTexture(DEFAULT_BUTTON_ICON)
  pushed:SetTexCoord(0.08, 0.92, 0.08, 0.92)
  button:SetPushedTexture(pushed)

  button.Icon = icon
  button.PushedIcon = pushed
  button.Border = border
  button.Background = background

  button:SetScript("OnClick", function(_, mouse_button)
    if mouse_button == "RightButton" then
      self:ShowContextMenu(button)
      return
    end
    if mouse_button == "LeftButton" then
      self:OpenPrimaryAddon()
    end
  end)
  button:SetScript("OnDragStart", function(btn)
    btn:SetScript("OnUpdate", function()
      self:UpdateButtonDrag()
    end)
  end)
  button:SetScript("OnDragStop", function(btn)
    btn:SetScript("OnUpdate", nil)
    self:UpdateButtonDrag()
  end)
  button:SetScript("OnHide", function(btn)
    btn:SetScript("OnUpdate", nil)
  end)
  button:SetScript("OnEnter", function(btn)
    self:ShowTooltip(btn)
  end)
  button:SetScript("OnLeave", function()
    if GameTooltip then
      GameTooltip:Hide()
    end
  end)

  self.button = button
  self:UpdateButtonAppearance()
  self:UpdateButtonPosition()
  return button
end

function shared:UpdateButtonAppearance()
  local button = self:EnsureButton()
  if not button then
    return
  end
  local primary, entries = self:GetPrimaryEntry()
  local icon_path = DEFAULT_BUTTON_ICON
  if primary then
    if #entries > 1 then
      icon_path = self:GetSharedIcon(entries)
    else
      icon_path = trim_text(primary.icon)
      if icon_path == "" then
        icon_path = DEFAULT_BUTTON_ICON
      end
    end
  end
  button.Icon:SetTexture(icon_path)
  button.PushedIcon:SetTexture(icon_path)
end

function shared:UpdateButtonPosition()
  local button = self:EnsureButton()
  if not button or not Minimap or not Minimap.GetCenter then
    return
  end
  local db = ensure_db()
  local angle = tonumber(db.angle) or 225
  local radius = (math.min(Minimap:GetWidth() or 140, Minimap:GetHeight() or 140) * 0.5) + 5
  local radians = math.rad(angle)
  button:ClearAllPoints()
  button:SetPoint("CENTER", Minimap, "CENTER", math.cos(radians) * radius, math.sin(radians) * radius)
end

function shared:UpdateButtonDrag()
  if not Minimap or not Minimap.GetCenter or not GetCursorPosition then
    return
  end
  local scale = Minimap:GetEffectiveScale() or 1
  local cursor_x, cursor_y = GetCursorPosition()
  local center_x, center_y = Minimap:GetCenter()
  if not cursor_x or not cursor_y or not center_x or not center_y then
    return
  end
  local dx = (cursor_x / scale) - center_x
  local dy = (cursor_y / scale) - center_y
  local db = ensure_db()
  db.angle = get_polar_angle_degrees(dy, dx)
  self:UpdateButtonPosition()
end

function shared:OpenPrimaryAddon()
  local primary = self:GetPrimaryEntry()
  if primary and type(primary.left_click) == "function" then
    safe_call(primary.left_click)
  end
end

function shared:ResetButtonPosition()
  local db = ensure_db()
  db.angle = 225
  self:UpdateButtonPosition()
end

function shared:IsStackHidden()
  local db = ensure_db()
  return db.settings.hide_stack == true
end

function shared:SetStackHidden(hidden)
  local db = ensure_db()
  db.settings.hide_stack = hidden == true
  self:Refresh()
  if self.settings_hub_panel and self.settings_hub_panel.RefreshContents then
    self.settings_hub_panel:RefreshContents(self:GetSettingsEntries(false))
  end
end

function shared:IsAddonHidden(addon_id)
  local db = ensure_db()
  local hidden_addons = db.settings.hidden_addons or {}
  return hidden_addons[tostring(addon_id or "")] == true
end

function shared:SetAddonHidden(addon_id, hidden)
  local key = tostring(addon_id or "")
  if key == "" then
    return
  end
  local db = ensure_db()
  db.settings.hidden_addons[key] = hidden == true
  self:Refresh()
  if self.settings_hub_panel and self.settings_hub_panel.RefreshContents then
    self.settings_hub_panel:RefreshContents(self:GetSettingsEntries(false))
  end
end

function shared:ShowTooltip(owner)
  if not GameTooltip then
    return
  end
  local primary, entries = self:GetPrimaryEntry()
  GameTooltip:SetOwner(owner, "ANCHOR_LEFT")

  if primary and type(primary.tooltip) == "function" then
    safe_call(primary.tooltip, owner, GameTooltip, self, primary)
    GameTooltip:Show()
    return
  end

  GameTooltip:SetText((#entries > 1) and "Kagrok Addons" or (primary and primary.name or "Kagrok Launcher"))
  if primary then
    GameTooltip:AddLine("Left Click: Open " .. primary.name, 1, 1, 1)
  else
    GameTooltip:AddLine("Left Click: Open primary addon", 1, 1, 1)
  end
  GameTooltip:AddLine("Right Click: Launcher menu", 1, 1, 1)
  GameTooltip:AddLine("Drag: Move along minimap", 0.65, 0.85, 1.0)
  if #entries > 1 then
    GameTooltip:AddLine(" ")
    for _, entry in ipairs(entries) do
      GameTooltip:AddLine(entry.name, 1.0, 0.82, 0.1)
    end
  end
  GameTooltip:Show()
end

function shared:HideContextMenu()
  if self.menu_frame then
    self.menu_frame:Hide()
  end
  if self.dismiss_frame then
    self.dismiss_frame:Hide()
  end
end

function shared:EnsureDismissFrame()
  if self.dismiss_frame then
    return self.dismiss_frame
  end
  local dismiss = CreateFrame("Button", nil, UIParent)
  dismiss:SetAllPoints(UIParent)
  dismiss:SetFrameStrata("DIALOG")
  dismiss:SetFrameLevel(1)
  dismiss:EnableMouse(true)
  dismiss:RegisterForClicks("LeftButtonUp")
  dismiss:SetScript("OnClick", function(_, mouse_button)
    if mouse_button == "LeftButton" then
      self:HideContextMenu()
    end
  end)
  dismiss:Hide()
  self.dismiss_frame = dismiss
  return dismiss
end

function shared:AcquireSectionTitle(index)
  local frame = self.menu_frame
  frame.section_titles = frame.section_titles or {}
  if frame.section_titles[index] then
    return frame.section_titles[index]
  end
  local title = frame.content:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  title:SetJustifyH("LEFT")
  title:SetTextColor(1.0, 0.82, 0.1)
  frame.section_titles[index] = title
  return title
end

function shared:AcquireDivider(index)
  local frame = self.menu_frame
  frame.dividers = frame.dividers or {}
  if frame.dividers[index] then
    return frame.dividers[index]
  end
  local divider = frame.content:CreateTexture(nil, "ARTWORK")
  divider:SetHeight(1)
  divider:SetColorTexture(0.24, 0.61, 0.78, 0.55)
  frame.dividers[index] = divider
  return divider
end

function shared:AcquireMenuButton(index)
  local frame = self.menu_frame
  frame.buttons = frame.buttons or {}
  if frame.buttons[index] then
    return frame.buttons[index]
  end
  local button = CreateFrame("Button", nil, frame.content)
  button:SetHeight(MENU_ROW_HEIGHT)

  local bg = button:CreateTexture(nil, "BACKGROUND")
  bg:SetAllPoints()
  bg:SetColorTexture(0.07, 0.09, 0.12, 0.82)

  local highlight = button:CreateTexture(nil, "HIGHLIGHT")
  highlight:SetAllPoints()
  highlight:SetColorTexture(1, 1, 1, 0.08)

  local text = button:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
  text:SetPoint("LEFT", button, "LEFT", 10, 0)
  text:SetPoint("RIGHT", button, "RIGHT", -10, 0)
  text:SetJustifyH("LEFT")
  text:SetTextColor(0.93, 0.91, 0.82)
  button.Text = text

  frame.buttons[index] = button
  return button
end

function shared:BuildSections(entries)
  local sections = {}
  for _, entry in ipairs(entries or self:GetEntries(false)) do
    local items = entry.menu_items
    if type(items) == "function" then
      items = safe_call(items, entry, self)
    end
    if type(items) == "table" and #items > 0 then
      table_insert(sections, {
        title = entry.name,
        items = items,
      })
    end
  end

  local seen = {}
  local shared_items = {}
  local settings_entries = self:GetSettingsEntries(false)
  if #settings_entries > 0 then
    seen["launcher_settings"] = true
    table_insert(shared_items, {
      key = "launcher_settings",
      text = "Open settings",
      order = 10,
      func = function()
        self:OpenSettings()
      end,
    })
  end
  seen["hide_minimap_button"] = true
  table_insert(shared_items, {
    key = "hide_minimap_button",
    text = self:IsStackHidden() and "Show minimap button" or "Hide minimap button",
    order = 999,
    func = function()
      self:SetStackHidden(not self:IsStackHidden())
    end,
  })
  for _, entry in ipairs(entries or self:GetEntries(false)) do
    local items = entry.shared_items
    if type(items) == "function" then
      items = safe_call(items, entry, self)
    end
    if type(items) == "table" then
      for _, item in ipairs(items) do
        local key = trim_text(item.key)
        if key ~= "" and not seen[key] then
          seen[key] = true
          table_insert(shared_items, item)
        end
      end
    end
  end
  table_sort(shared_items, function(a, b)
    local ao = tonumber(a.order) or 0
    local bo = tonumber(b.order) or 0
    if ao ~= bo then
      return ao < bo
    end
    return tostring(a.text or "") < tostring(b.text or "")
  end)
  if #shared_items > 0 then
    table_insert(sections, {
      title = "Shared",
      items = shared_items,
    })
  end
  return sections
end

function shared:RefreshMenuLayout(entries)
  local frame = self.menu_frame
  if not frame then
    return
  end

  local sections = self:BuildSections(entries)
  local active_entry_count = #(entries or self:GetEntries(false))
  local use_scrollbar = active_entry_count >= MIN_SCROLLBAR_ENTRY_COUNT
  local title_count = 0
  local divider_count = 0
  local button_count = 0
  local y = -8
  local content_width = use_scrollbar and (MENU_WIDTH - 42) or (MENU_WIDTH - 22)

  for _, section in ipairs(sections) do
    title_count = title_count + 1
    local title = self:AcquireSectionTitle(title_count)
    title:ClearAllPoints()
    title:SetPoint("TOPLEFT", frame.content, "TOPLEFT", 4, y)
    title:SetWidth(content_width - 8)
    title:SetText(section.title)
    title:Show()
    y = y - 16

    divider_count = divider_count + 1
    local divider = self:AcquireDivider(divider_count)
    divider:ClearAllPoints()
    divider:SetPoint("TOPLEFT", frame.content, "TOPLEFT", 4, y)
    divider:SetPoint("TOPRIGHT", frame.content, "TOPLEFT", content_width, y)
    divider:Show()
    y = y - 8

    for _, item in ipairs(section.items) do
      button_count = button_count + 1
      local button = self:AcquireMenuButton(button_count)
      button:ClearAllPoints()
      button:SetPoint("TOPLEFT", frame.content, "TOPLEFT", 4, y)
      button:SetPoint("TOPRIGHT", frame.content, "TOPLEFT", content_width, y)
      button.Text:SetText(tostring(item.text or "Option"))
      button:SetEnabled(item.disabled ~= true)
      button:SetAlpha((item.disabled == true) and 0.5 or 1)
      button:SetScript("OnClick", function()
        self:HideContextMenu()
        if type(item.func) == "function" then
          safe_call(item.func)
        end
      end)
      button:Show()
      y = y - (MENU_ROW_HEIGHT + 2)
    end
    y = y - 6
  end

  local content_height = math.max(1, -y + 8)
  frame.content:SetSize(content_width, content_height)
  frame.scroll:SetVerticalScroll(0)
  if frame.scroll.ScrollBar then
    frame.scroll.ScrollBar:SetShown(use_scrollbar)
  end
  frame.scroll:ClearAllPoints()
  frame.scroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -30)
  frame.scroll:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", use_scrollbar and -28 or -8, 8)
  frame:SetHeight(use_scrollbar and MENU_MAX_HEIGHT or (content_height + 44))

  for index = title_count + 1, #(frame.section_titles or {}) do
    frame.section_titles[index]:Hide()
  end
  for index = divider_count + 1, #(frame.dividers or {}) do
    frame.dividers[index]:Hide()
  end
  for index = button_count + 1, #(frame.buttons or {}) do
    frame.buttons[index]:Hide()
  end
end

function shared:EnsureContextMenu()
  if self.menu_frame then
    return self.menu_frame
  end

  local frame = create_frame_with_optional_template("Frame", "KagrokSharedLauncherContextMenu", UIParent, "BackdropTemplate")
  frame:SetSize(MENU_WIDTH, 140)
  frame:SetFrameStrata("DIALOG")
  frame:SetFrameLevel(10)
  frame:SetClampedToScreen(true)
  create_backdrop(frame, { 0.03, 0.04, 0.06, 0.96 }, { 0.85, 0.7, 0.18, 1 })

  local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  title:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -10)
  title:SetTextColor(1.0, 0.82, 0.1)
  frame.TitleText = title

  local scroll = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate")
  scroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -30)
  scroll:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -28, 8)
  scroll:EnableMouseWheel(true)
  scroll:SetScript("OnMouseWheel", function(self_frame, delta)
    local current = self_frame:GetVerticalScroll() or 0
    local step = 18
    local next_value = current - (delta * step)
    if next_value < 0 then
      next_value = 0
    end
    local max_scroll = math.max(0, (frame.content:GetHeight() or 0) - (scroll:GetHeight() or 0))
    if next_value > max_scroll then
      next_value = max_scroll
    end
    self_frame:SetVerticalScroll(next_value)
  end)
  frame.scroll = scroll

  local content = CreateFrame("Frame", nil, scroll)
  content:SetSize(MENU_WIDTH - 42, 1)
  scroll:SetScrollChild(content)
  frame.content = content

  frame:Hide()
  self.menu_frame = frame
  return frame
end

function shared:ShowContextMenu(owner)
  local primary, entries = self:GetPrimaryEntry()
  if not primary then
    self:HideContextMenu()
    return
  end

  local dismiss = self:EnsureDismissFrame()
  local frame = self:EnsureContextMenu()
  frame.TitleText:SetText((#entries > 1) and "Kagrok Addons" or primary.name)
  self:RefreshMenuLayout(entries)

  if frame:IsShown() then
    self:HideContextMenu()
    return
  end

  frame:ClearAllPoints()
  if owner and owner.GetRight and owner.GetBottom then
    frame:SetPoint("TOPRIGHT", owner, "BOTTOMRIGHT", 10, -4)
  else
    local scale = UIParent and UIParent.GetEffectiveScale and UIParent:GetEffectiveScale() or 1
    local cursor_x, cursor_y = GetCursorPosition()
    local x = (cursor_x or 0) / scale
    local y = (cursor_y or 0) / scale
    frame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x + 8, y - 8)
  end

  dismiss:Show()
  frame:Show()
end

function shared:Refresh()
  local button = self:EnsureButton()
  if not button then
    return nil
  end
  local primary = self:GetPrimaryEntry()
  local visible = (primary ~= nil) and not self:IsStackHidden()
  button:SetShown(visible)
  if visible then
    self:UpdateButtonAppearance()
    self:UpdateButtonPosition()
    if self.menu_frame and self.menu_frame:IsShown() then
      self:RefreshMenuLayout(self:GetEntries(false))
    end
  else
    self:HideContextMenu()
  end
  return button
end

function shared:EnsureSettingsBootstrap()
  if self.settings_bootstrap then
    return self.settings_bootstrap
  end
  local frame = CreateFrame("Frame")
  frame:RegisterEvent("PLAYER_LOGIN")
  frame:RegisterEvent("PLAYER_ENTERING_WORLD")
  frame:SetScript("OnEvent", function(_, event)
    if event == "PLAYER_LOGIN" then
      self.settings_ready = true
    elseif event == "PLAYER_ENTERING_WORLD" then
      self.settings_ready = true
      self:ScheduleSettingsFinalize()
    end
  end)
  self.settings_bootstrap = frame
  return frame
end

function shared:ScheduleSettingsFinalize()
  if self.settings_finalized or self.settings_finalize_pending then
    return
  end

  self.settings_finalize_pending = true
  local function finalize()
    self.settings_finalize_pending = false
    if not self.settings_finalized then
      self:FinalizeSettingsPages()
    end
  end

  if C_Timer and type(C_Timer.After) == "function" then
    C_Timer.After(0, finalize)
  else
    finalize()
  end
end

function shared:AcquireSettingsHubRow(panel, index)
  panel.list_rows = panel.list_rows or {}
  if panel.list_rows[index] then
    return panel.list_rows[index]
  end

  local row = create_frame_with_optional_template("Frame", nil, panel, "BackdropTemplate")
  row:SetHeight(56)
  if row.SetBackdrop then
    row:SetBackdrop({
      bgFile = "Interface\\Buttons\\WHITE8X8",
      edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
      tile = false,
      edgeSize = 12,
      insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    row:SetBackdropColor(0.05, 0.05, 0.07, 0.90)
    row:SetBackdropBorderColor(0.20, 0.20, 0.24, 0.90)
  end

  row.header = row:CreateTexture(nil, "ARTWORK")
  row.header:SetColorTexture(1, 1, 1, 0.04)
  row.header:SetPoint("TOPLEFT", row, "TOPLEFT", 4, -4)
  row.header:SetPoint("TOPRIGHT", row, "TOPRIGHT", -4, -4)
  row.header:SetHeight(20)

  row.icon = row:CreateTexture(nil, "ARTWORK")
  row.icon:SetSize(26, 26)
  row.icon:SetPoint("LEFT", row, "LEFT", 12, 0)

  row.order = row:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
  row.order:SetPoint("LEFT", row.icon, "RIGHT", 10, 0)
  row.order:SetWidth(20)
  row.order:SetJustifyH("LEFT")
  row.order:SetTextColor(0.95, 0.82, 0.18)

  row.title = row:CreateFontString(nil, "ARTWORK", "GameFontNormal")
  row.title:SetPoint("TOPLEFT", row.order, "TOPRIGHT", 6, -4)
  row.title:SetWidth(430)
  row.title:SetJustifyH("LEFT")

  row.meta = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
  row.meta:SetPoint("TOPLEFT", row.title, "BOTTOMLEFT", 0, -4)
  row.meta:SetWidth(430)
  row.meta:SetJustifyH("LEFT")
  row.meta:SetSpacing(1)

  row.priority = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
  row.priority:SetPoint("RIGHT", row, "RIGHT", -14, 0)
  row.priority:SetJustifyH("RIGHT")
  row.priority:SetTextColor(0.82, 0.82, 0.82)

  panel.list_rows[index] = row
  return row
end

local function create_settings_section(parent, label, x, y, width)
  local frame = create_frame_with_optional_template("Frame", nil, parent, "BackdropTemplate")
  frame:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
  frame:SetWidth(width)
  if frame.SetBackdrop then
    frame:SetBackdrop({
      bgFile = "Interface\\Buttons\\WHITE8X8",
      edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
      tile = false,
      edgeSize = 12,
      insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    frame:SetBackdropColor(0.05, 0.05, 0.07, 0.90)
    frame:SetBackdropBorderColor(0.20, 0.20, 0.24, 0.90)
  end

  frame.header = frame:CreateTexture(nil, "ARTWORK")
  frame.header:SetColorTexture(1, 1, 1, 0.04)
  frame.header:SetPoint("TOPLEFT", frame, "TOPLEFT", 4, -4)
  frame.header:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -4, -4)
  frame.header:SetHeight(24)

  frame.title = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
  frame.title:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -10)
  frame.title:SetText(label or "")

  frame.pad = 12
  frame.innerY = -36
  return frame
end

local function add_section_note(frame, text, width)
  local note = frame:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
  note:SetPoint("TOPLEFT", frame, "TOPLEFT", frame.pad, frame.innerY)
  note:SetWidth(width or (frame:GetWidth() - (frame.pad * 2)))
  note:SetJustifyH("LEFT")
  note:SetSpacing(2)
  note:SetText(text or "")
  frame.innerY = frame.innerY - ((note:GetStringHeight() or 18) + 10)
  return note
end

local function finish_settings_section(frame, extra_bottom)
  local padding = extra_bottom or 12
  local height = math.abs(frame.innerY) + padding
  frame:SetHeight(height)
  return height
end

local function create_settings_check(parent, label, initial, on_click)
  local cb = CreateFrame("CheckButton", nil, parent, "InterfaceOptionsCheckButtonTemplate")
  cb.Text:SetText(label or "")
  cb:SetChecked(initial == true)
  cb:SetScript("OnClick", function(self)
    if type(on_click) == "function" then
      on_click(self:GetChecked() == true)
    end
  end)
  return cb
end

function shared:EnsureSettingsHubPanel()
  if self.settings_hub_panel then
    return self.settings_hub_panel
  end

  local panel = CreateFrame("Frame", "KagrokSharedLauncherSettingsPanel")
  panel.name = "Kagrok's Addons"

  local title = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
  title:SetPoint("TOPLEFT", panel, "TOPLEFT", 16, -16)
  title:SetText("Kagrok's Addons")

  local desc = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
  desc:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8)
  desc:SetWidth(760)
  desc:SetJustifyH("LEFT")
  desc:SetSpacing(2)
  desc:SetText("Shared launcher settings for Kagrok addons. Subcategories below are listed in launcher priority order.")

  local scroll = CreateFrame("ScrollFrame", nil, panel, "UIPanelScrollFrameTemplate")
  scroll:SetPoint("TOPLEFT", desc, "BOTTOMLEFT", -4, -8)
  scroll:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -26, 16)
  scroll:EnableMouseWheel(true)
  scroll:SetScript("OnMouseWheel", function(self_frame, delta)
    local range = self_frame:GetVerticalScrollRange() or 0
    local current = self_frame:GetVerticalScroll() or 0
    local next_value = math.max(0, math.min(range, current - ((delta or 0) * 28)))
    self_frame:SetVerticalScroll(next_value)
  end)

  local content = CreateFrame("Frame", nil, scroll)
  content:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0)
  content:SetPoint("RIGHT", scroll, "RIGHT", 0, 0)
  content:SetSize(820, 420)
  scroll:SetScrollChild(content)
  scroll:HookScript("OnSizeChanged", function(_, width)
    if content and width and width > 0 then
      content:SetWidth(width - 2)
    end
  end)

  local layout_padding = 12
  local section_gap = 3
  local content_width = 820
  local inner_width = content_width - (layout_padding * 2)
  local left_width = math.floor((inner_width - section_gap) * 0.42) - 35
  local right_width = left_width
  local left_x = layout_padding
  local right_x = left_x + left_width + section_gap
  local full_width = left_width + right_width + section_gap
  local cursor_y = -6

  local launcher_section = create_settings_section(content, "Launcher", left_x, cursor_y, left_width)
  local summary_section = create_settings_section(content, "Settings", right_x, cursor_y, right_width)

  local show_stack = create_settings_check(launcher_section, "Show shared minimap button stack", not self:IsStackHidden(), function(value)
    self:SetStackHidden(not value)
  end)
  show_stack:SetPoint("TOPLEFT", launcher_section, "TOPLEFT", launcher_section.pad, launcher_section.innerY)
  launcher_section.innerY = launcher_section.innerY - 28

  local reset = CreateFrame("Button", nil, launcher_section, "UIPanelButtonTemplate")
  reset:SetSize(154, 24)
  reset:SetPoint("TOPLEFT", launcher_section, "TOPLEFT", launcher_section.pad, launcher_section.innerY - 2)
  reset:SetText("Reset minimap button")
  reset:SetScript("OnClick", function()
    self:ResetButtonPosition()
  end)
  launcher_section.innerY = launcher_section.innerY - 38
  add_section_note(launcher_section, "Reset the shared minimap button position for every registered Kagrok addon.", left_width - (launcher_section.pad * 2))

  local summary_text = summary_section:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
  summary_text:SetPoint("TOPLEFT", summary_section, "TOPLEFT", summary_section.pad, summary_section.innerY)
  summary_text:SetWidth(right_width - (summary_section.pad * 2))
  summary_text:SetJustifyH("LEFT")
  summary_text:SetSpacing(2)
  summary_text:SetText("")
  summary_section.innerY = summary_section.innerY - 68
  add_section_note(summary_section, "Use the shared settings entry from the minimap launcher to open this hub or the only available addon page.", right_width - (summary_section.pad * 2))

  local row_height = math.max(finish_settings_section(launcher_section), finish_settings_section(summary_section))
  launcher_section:SetHeight(row_height)
  summary_section:SetHeight(row_height)
  cursor_y = cursor_y - row_height - section_gap

  local list_section = create_settings_section(content, "Registered Addons", left_x, cursor_y, full_width)
  local list_note = list_section:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
  list_note:SetPoint("TOPLEFT", list_section, "TOPLEFT", list_section.pad, list_section.innerY)
  list_note:SetWidth(full_width - (list_section.pad * 2))
  list_note:SetJustifyH("LEFT")
  list_note:SetSpacing(2)
  list_note:SetText("Each addon keeps its own settings page below this parent. This list shows the shared launcher order.")
  list_section.rows_start_y = list_section.innerY - ((list_note:GetStringHeight() or 18) + 12)
  list_section.innerY = list_section.rows_start_y

  panel.RefreshContents = function(frame, entries)
    local rows = entries or self:GetSettingsEntries(false)
    local settings_target = (#rows > 1) and "Kagrok's Addons" or ((rows[1] and rows[1].name) or "this addon")
    if frame.ShowStackCheck then
      frame.ShowStackCheck:SetChecked(not self:IsStackHidden())
    end
    summary_text:SetText(string.format("Registered settings pages: %d\nOpen settings destination: %s\nMinimap stack: %s", #rows, settings_target, self:IsStackHidden() and "Hidden" or "Shown"))

    local y = frame.listSection.rows_start_y
    local row_width = full_width - 24
    for index, entry in ipairs(rows) do
      local row = self:AcquireSettingsHubRow(frame.listSection, index)
      row:ClearAllPoints()
      row:SetPoint("TOPLEFT", frame.listSection, "TOPLEFT", 12, y)
      row:SetWidth(row_width)
      row.icon:SetTexture(trim_text(entry.icon) ~= "" and entry.icon or DEFAULT_BUTTON_ICON)
      row.order:SetText(string.format("%d.", index))
      row.title:SetText(entry.name or entry.id or "Unknown Addon")
      row.meta:SetText(string.format("Settings page: %s\nMinimap entry: %s", entry.name or entry.id or "Unknown Addon", self:IsAddonHidden(entry.id) and "Hidden" or "Shown"))
      row.priority:SetText(string.format("Priority %d", get_effective_priority(entry)))
      row:Show()
      y = y - 60
    end
    for index = #rows + 1, #(frame.listSection.list_rows or {}) do
      frame.listSection.list_rows[index]:Hide()
    end
    frame.listSection:SetHeight(math.max(120, math.abs(y) + 20))
    content:SetHeight(math.abs(cursor_y) + frame.listSection:GetHeight() + 24)
  end

  panel.ScrollFrame = scroll
  panel.ScrollContent = content
  panel.LauncherSection = launcher_section
  panel.ShowStackCheck = show_stack
  panel.SummarySection = summary_section
  panel.listSection = list_section

  self.settings_hub_panel = panel
  return panel
end

function shared:FinalizeSettingsPages()
  if self.settings_finalized then
    return true
  end

  local entries = self:GetSettingsEntries(false)
  if #entries == 0 then
    return false
  end

  local supports_canvas_categories = Settings
    and type(Settings.RegisterCanvasLayoutCategory) == "function"
    and type(Settings.RegisterAddOnCategory) == "function"
  local supports_subcategories = supports_canvas_categories and type(Settings.RegisterCanvasLayoutSubcategory) == "function"

  if supports_canvas_categories then
    if #entries == 1 or not supports_subcategories then
      for _, entry in ipairs(entries) do
        local panel = safe_call(entry.get_panel, entry, self)
        if panel then
          local category = Settings.RegisterCanvasLayoutCategory(panel, entry.name)
          Settings.RegisterAddOnCategory(category)
          entry.category = category
          entry.category_name = entry.name
          if type(entry.on_registered) == "function" then
            safe_call(entry.on_registered, category, nil)
          end
        end
      end
    else
      local hub_panel = self:EnsureSettingsHubPanel()
      local hub_category = Settings.RegisterCanvasLayoutCategory(hub_panel, hub_panel.name)
      Settings.RegisterAddOnCategory(hub_category)
      self.settings_hub_category = hub_category
      hub_panel:RefreshContents(entries)

      for index, entry in ipairs(entries) do
        local panel = safe_call(entry.get_panel, entry, self)
        if panel then
          local subcategory = Settings.RegisterCanvasLayoutSubcategory(hub_category, panel, entry.name)
          if subcategory and subcategory.SetOrder then
            subcategory:SetOrder(index)
          end
          entry.category = subcategory
          entry.category_name = entry.name
          if type(entry.on_registered) == "function" then
            safe_call(entry.on_registered, subcategory, hub_category)
          end
        end
      end
    end
  else
    for _, entry in ipairs(entries) do
      local panel = safe_call(entry.get_panel, entry, self)
      if panel and type(InterfaceOptions_AddCategory) == "function" then
        InterfaceOptions_AddCategory(panel)
        entry.category = panel
        entry.category_name = panel.name or entry.name
        if type(entry.on_registered) == "function" then
          safe_call(entry.on_registered, panel, nil)
        end
      end
    end
  end

  self.settings_finalized = true
  return true
end

function shared:OpenSettings(addon_id)
  if not self.settings_finalized then
    self:FinalizeSettingsPages()
  end

  local entries = self:GetSettingsEntries(false)
  local entry = nil
  if addon_id then
    self.settings_registry = self.settings_registry or {}
    entry = self.settings_registry[tostring(addon_id or "")]
  else
    if #entries > 1 and self.settings_hub_category then
      entry = nil
    else
      entry = entries[1]
    end
  end

  local target = nil
  local fallback_name = nil
  if entry and entry.category then
    target = entry.category
    fallback_name = entry.category_name or entry.name
  elseif self.settings_hub_category then
    target = self.settings_hub_category
    fallback_name = "Kagrok's Addons"
  elseif entry then
    fallback_name = entry.category_name or entry.name
  end

  if Settings and Settings.OpenToCategory then
    if type(target) == "table" and type(target.GetID) == "function" then
      Settings.OpenToCategory(target:GetID())
      return true
    end
    if type(target) == "table" and target.ID then
      Settings.OpenToCategory(target.ID)
      return true
    end
    if fallback_name and fallback_name ~= "" then
      Settings.OpenToCategory(fallback_name)
      return true
    end
  end

  if InterfaceOptionsFrame_OpenToCategory then
    InterfaceOptionsFrame_OpenToCategory(target or fallback_name)
    InterfaceOptionsFrame_OpenToCategory(target or fallback_name)
    return true
  end
  return false
end

shared.registry = shared.registry or {}
shared.settings_registry = shared.settings_registry or {}
shared.__module_version = MODULE_VERSION
