local addonName, ns = ...

local MM = {}
ns.MM = MM
_G.MurlocMinder = MM

local ADDON_NAME = addonName or "MurlocMinder"
local TITLE_VERSION_FALLBACK = "0.1.7"
local ICON_MURLOC = "Interface\\Icons\\INV_Misc_Head_Murloc_01"
local SOCIAL_MEDIA_ROOT = "Interface\\AddOns\\" .. ADDON_NAME .. "\\Media\\Social\\"
local MINIMAP_TOOLTIP_COLORS = {
    hunger = { 0.38, 0.92, 0.57 },
    joy = { 0.35, 0.72, 1.0 },
    energy = { 0.97, 0.8, 0.31 },
    cleanliness = { 0.76, 0.58, 0.95 },
    hatch = { 0.96, 0.42, 0.72 },
}
local SOCIAL_LINK_ENTRIES = {
    {
        key = "curseforge",
        label = "CurseForge",
        url = "https://www.curseforge.com/members/kagrok/projects",
        tooltip = "Check out my other projects",
        icon = SOCIAL_MEDIA_ROOT .. "curseforge.png",
        accent = { 0.95, 0.42, 0.12 },
    },
    {
        key = "discord",
        label = "Discord",
        url = "http://discord.gg/U2TeUF9y3K",
        tooltip = "Join my community",
        badge = "D",
        accent = { 0.35, 0.42, 0.95 },
    },
    {
        key = "bluesky",
        label = "Bluesky",
        url = "https://bsky.app/profile/kagrok.bsky.social",
        tooltip = "Join me on BlueSky",
        icon = SOCIAL_MEDIA_ROOT .. "bluesky.jpeg",
        accent = { 0.09, 0.62, 0.97 },
    },
    {
        key = "twitch",
        label = "Twitch",
        url = "https://www.twitch.tv/kagrok",
        tooltip = "Follow me on Twitch",
        icon = SOCIAL_MEDIA_ROOT .. "twitch.png",
        accent = { 0.57, 0.36, 0.93 },
    },
    {
        key = "youtube",
        label = "YouTube",
        url = "https://www.youtube.com/@Kagrok",
        tooltip = "Follow me on YouTube",
        icon = SOCIAL_MEDIA_ROOT .. "youtube.png",
        accent = { 0.92, 0.16, 0.14 },
    },
    {
        key = "patreon",
        label = "Patreon",
        url = "https://www.patreon.com/cw/Kagrok",
        tooltip = "Support me!",
        icon = SOCIAL_MEDIA_ROOT .. "patreon.png",
        accent = { 0.96, 0.39, 0.29 },
    },
    {
        key = "buymeacoffee",
        label = "Buy Me A Coffee",
        url = "https://buymeacoffee.com/kagrok",
        tooltip = "Support me!",
        icon = SOCIAL_MEDIA_ROOT .. "bmac.png",
        accent = { 1.0, 0.86, 0.22 },
    },
}

local defaults = {
    profile = {
        welcomeSeen = false,
        minimap = {
            hide = false,
            angle = 225,
        },
        frame = {
            point = "CENTER",
            x = 0,
            y = 0,
            width = 1120,
            height = 700,
            scale = 1.0,
        },
        store = {
            shells = 120,
            inventory = {
                kelpSnack = 3,
                tideWorm = 1,
                bubbleBiscuit = 0,
                reefJerky = 0,
                tideFeast = 0,
            },
            customization = {
                selectedPage = "habitats",
                activeHabitatBackground = "auto",
                habitatOwned = {
                    auto = true,
                },
            },
        },
        pet = {
            name = "Mrrgl",
            stage = "egg",
            ageMinutes = 0,
            hunger = 85,
            joy = 75,
            energy = 90,
            cleanliness = 80,
            hatchProgress = 8,
            personality = "Unknown",
        },
        activityLog = {},
        dev = {
            showDevTab = false,
        },
    },
}

local function DeepCopy(src)
    if type(src) ~= "table" then
        return src
    end

    local dest = {}
    for key, value in pairs(src) do
        dest[key] = DeepCopy(value)
    end
    return dest
end

local function MergeDefaults(dest, src)
    for key, value in pairs(src) do
        if type(value) == "table" then
            if type(dest[key]) ~= "table" then
                dest[key] = {}
            end
            MergeDefaults(dest[key], value)
        elseif dest[key] == nil then
            dest[key] = value
        end
    end
end

local function TrimString(str)
    return str and str:match("^%s*(.-)%s*$") or ""
end

-- Sound profiles
-- Keep these centralized so action/autonomy assignments are easy to tune.
local CARE_ACTION_SOUND_POOLS = {
    play = {
        { index = 13 },
        { index = 15 },
        { index = 16 },
        { index = 17 },
        { index = 19 },
    },
    feed = {
        { index = 119 },
        { index = 120 },
        { index = 121 },
        { index = 122 },
        { index = 123 },
    },
    clean = {
        { index = 227 },
        { index = 228 },
        { index = 229 },
        { index = 230 },
        { index = 231 },
        { index = 232 },
    },
    rest = {
        { index = 121 },
        { index = 213 },
        { index = 215 },
        { index = 217 },
        { index = 218 },
        { index = 219 },
        { index = 220 },
        { index = 221 },
        { index = 222 },
        { index = 223 },
        { index = 224 },
        { index = 225 },
    },
}

local AUTONOMOUS_SOUND_POOLS = {
    positive = {
        { index = 6 },
        { index = 9 },
        { index = 10 },
        { index = 11 },
        { index = 12 },
        { index = 174 },
        { index = 126 },
    },
    negative = {
        { index = 1 },
        { index = 2 },
        { index = 4 },
        { index = 14 },
        { index = 20 },
        { index = 21 },
        { index = 111 },
        { index = 113 },
        { index = 115 },
        { index = 116 },
        { index = 117 },
        { index = 123 },
    },
}

local UI_SOUND_PROFILE = {
    button = { "IG_MAINMENU_OPTION_CHECKBOX_ON", "U_CHAT_SCROLL_BUTTON" },
    popup_open = { "IG_MAINMENU_OPEN", "IG_CHARACTER_INFO_OPEN" },
    popup_close = { "IG_MAINMENU_CLOSE", "GS_TITLE_OPTION_OK" },
    purchase = { "LOOT_WINDOW_COIN_SOUND", "GS_TITLE_OPTION_OK" },
    error = { "IG_MAINMENU_OPTION_CHECKBOX_OFF", "IG_ABILITY_PAGE_TURN" },
}

local function IsCVarEnabled(name, defaultValue)
    if type(GetCVar) ~= "function" then
        return defaultValue == true
    end
    local ok, value = pcall(GetCVar, name)
    if (not ok) or value == nil then
        return defaultValue == true
    end
    if type(value) == "number" then
        return value ~= 0
    end
    local text = tostring(value)
    if text == "" then
        return defaultValue == true
    end
    text = string.lower(text)
    return text == "1" or text == "true" or text == "yes" or text == "on"
end

function MM:IsAddonSFXEnabled()
    return IsCVarEnabled("Sound_EnableAllSound", true) and IsCVarEnabled("Sound_EnableSFX", true)
end

function MM:IsMainPanelOpen()
    return self.frame and self.frame.IsShown and self.frame:IsShown()
end

local function BuildSoundPathCandidates(path)
    local raw = tostring(path or "")
    if raw == "" then
        return {}
    end

    local list = {}
    local seen = {}
    local function push(candidate)
        local text = tostring(candidate or "")
        if text == "" or seen[text] then
            return
        end
        seen[text] = true
        list[#list + 1] = text
    end

    push(raw)

    local asForward = raw:gsub("\\", "/")
    local asBack = raw:gsub("/", "\\")
    push(asForward)
    push(asBack)

    local forwardLower = string.lower(asForward)
    if forwardLower:match("^sound/") then
        push("Sound/" .. asForward:sub(7))
        push("Sound\\" .. asBack:sub(7))
    elseif forwardLower:match("^sound\\\\") then
        push("Sound\\" .. asBack:sub(7))
        push("Sound/" .. asForward:sub(7))
    elseif not forwardLower:match("^interface/") and not forwardLower:match("^interface\\\\") then
        push("sound/" .. asForward)
        push("sound\\" .. asBack)
        push("Sound/" .. asForward)
        push("Sound\\" .. asBack)
    end

    return list
end

local function DidSoundPlay(ok, willPlay, soundHandle)
    if not ok then
        return false
    end
    if willPlay == true then
        return true
    end
    if type(willPlay) == "number" and willPlay > 0 then
        return true
    end
    if type(soundHandle) == "number" and soundHandle > 0 then
        return true
    end
    return false
end

local function BuildChannelCandidates(preferredChannel)
    local channels = {}
    local seen = {}
    local seenNil = false
    local function push(channel)
        if channel == nil then
            if seenNil then
                return
            end
            seenNil = true
        else
            if seen[channel] then
                return
            end
            seen[channel] = true
        end
        channels[#channels + 1] = channel
    end

    push(preferredChannel)
    push(nil)
    push("SFX")
    push("Master")
    return channels
end

local function TryPlaySoundKitVariants(id, preferredChannel)
    local numericID = tonumber(id)
    if not numericID or numericID <= 0 then
        return false
    end

    local channels = BuildChannelCandidates(preferredChannel)
    local soundFunctions = {}
    if type(PlaySound) == "function" then
        soundFunctions[#soundFunctions + 1] = PlaySound
    end
    if C_Sound and type(C_Sound.PlaySound) == "function" and C_Sound.PlaySound ~= PlaySound then
        soundFunctions[#soundFunctions + 1] = C_Sound.PlaySound
    end
    if type(PlaySoundKitID) == "function" then
        soundFunctions[#soundFunctions + 1] = PlaySoundKitID
    end

    for _, fn in ipairs(soundFunctions) do
        for _, channel in ipairs(channels) do
            local ok, willPlay, soundHandle
            if channel ~= nil then
                ok, willPlay, soundHandle = pcall(fn, numericID, channel)
            else
                ok, willPlay, soundHandle = pcall(fn, numericID)
            end
            if DidSoundPlay(ok, willPlay, soundHandle) then
                return true
            end
        end
    end

    return false
end

local function TryPlaySoundFileVariants(path, id, channel)
    if type(PlaySoundFile) ~= "function" then
        return false
    end

    local channels = BuildChannelCandidates(channel)
    local numericID = tonumber(id)
    if numericID and numericID > 0 then
        for _, tryChannel in ipairs(channels) do
            local ok, willPlay, soundHandle
            if tryChannel ~= nil then
                ok, willPlay, soundHandle = pcall(PlaySoundFile, numericID, tryChannel)
            else
                ok, willPlay, soundHandle = pcall(PlaySoundFile, numericID)
            end
            if DidSoundPlay(ok, willPlay, soundHandle) then
                return true
            end
        end
    end

    for _, candidate in ipairs(BuildSoundPathCandidates(path)) do
        for _, tryChannel in ipairs(channels) do
            local ok, willPlay, soundHandle
            if tryChannel ~= nil then
                ok, willPlay, soundHandle = pcall(PlaySoundFile, candidate, tryChannel)
            else
                ok, willPlay, soundHandle = pcall(PlaySoundFile, candidate)
            end
            if DidSoundPlay(ok, willPlay, soundHandle) then
                return true
            end
        end
    end

    return false
end

function MM:GetSoundEntryByAssignmentIndex(index)
    local idx = tonumber(index)
    if not idx or idx < 1 then
        return nil
    end
    if type(self.GetDevMurlocSoundEntries) ~= "function" then
        return nil
    end
    local catalog = self:GetDevMurlocSoundEntries()
    local entry = type(catalog) == "table" and catalog[idx] or nil
    if type(entry) ~= "table" then
        return nil
    end
    local out = {
        id = tonumber(entry.id),
        mode = tostring(entry.mode or "file"),
    }
    if out.mode == "file" then
        out.path = tostring(entry.key or entry.path or entry.display or "")
    end
    return out
end

function MM:ResolveAssignedSoundEntry(assignedEntry)
    if type(assignedEntry) ~= "table" then
        local id = tonumber(assignedEntry)
        if id and id > 0 then
            return { id = id, mode = "kit", path = nil }
        end
        return nil
    end

    local fromIndex = self:GetSoundEntryByAssignmentIndex(assignedEntry.index)
    if fromIndex then
        return fromIndex
    end

    local resolved = {
        id = tonumber(assignedEntry.id),
        mode = tostring(assignedEntry.mode or ""),
        path = tostring(assignedEntry.path or ""),
    }
    if resolved.mode == "" then
        resolved.mode = (resolved.path ~= "" and "file") or "kit"
    end
    if resolved.id and resolved.id > 0 then
        return resolved
    end
    if resolved.path ~= "" then
        return resolved
    end
    return nil
end

function MM:PlayAddonSound(soundID, mode, path)
    local id = tonumber(soundID)
    if not self:IsAddonSFXEnabled() then
        return false
    end

    local entryMode = mode or ((path and path ~= "") and "file" or "kit")
    local played
    if entryMode == "file" then
        played = TryPlaySoundFileVariants(path, id, "SFX")
        if played then
            return true
        end
    end

    played = TryPlaySoundKitVariants(id, "SFX")
    if not played then
        played = TryPlaySoundFileVariants(path, id, "SFX")
    end
    return played
end

function MM:GetCareActionSoundPool(actionKey)
    local key = tostring(actionKey or "")
    if key == "egg_soothe" then
        key = "play"
    elseif key == "egg_warm" then
        key = "rest"
    elseif key == "egg_clean" then
        key = "clean"
    end
    return CARE_ACTION_SOUND_POOLS[key]
end

function MM:PlayCareActionButtonSound(actionKey)
    local pool = self:GetCareActionSoundPool(actionKey)
    if type(pool) ~= "table" or #pool == 0 then
        return false
    end
    local entry = pool[math.random(1, #pool)]
    local sound = self:ResolveAssignedSoundEntry(entry)
    if type(sound) ~= "table" then
        return false
    end
    return self:PlayAddonSound(sound.id, sound.mode, sound.path)
end

function MM:GetAutonomousSoundPool(polarity)
    local key = (tostring(polarity or "") == "negative") and "negative" or "positive"
    return AUTONOMOUS_SOUND_POOLS[key]
end

function MM:PlayAutonomousActionSound(polarity)
    if not self:IsMainPanelOpen() then
        return false
    end
    local pool = self:GetAutonomousSoundPool(polarity)
    if type(pool) ~= "table" or #pool == 0 then
        return false
    end
    local index = math.random(1, #pool)
    local entry = pool[index]
    local sound = self:ResolveAssignedSoundEntry(entry)
    if type(sound) ~= "table" then
        return false
    end
    return self:PlayAddonSound(sound.id, sound.mode, sound.path)
end

function MM:PlayDevPreviewSound(soundID, mode, path)
    local id = tonumber(soundID)
    if (not id or id <= 0) and tostring(path or "") == "" then
        return false
    end

    local entryMode = mode or ((tostring(path or "") ~= "") and "file" or "kit")
    local played
    if entryMode == "file" then
        played = TryPlaySoundFileVariants(path, id, "Master")
        if played then
            return true
        end
    end

    played = TryPlaySoundKitVariants(id, "Master")
    if not played then
        played = TryPlaySoundFileVariants(path, id, "Master")
    end
    return played
end

local function ResolveSoundKitIDFromSpec(spec)
    if type(spec) == "number" then
        return spec
    end
    if type(spec) ~= "string" or type(SOUNDKIT) ~= "table" then
        return nil
    end
    local soundID = tonumber(SOUNDKIT[spec])
    if soundID and soundID > 0 then
        return soundID
    end
    return nil
end

function MM:PlayUISound(kind)
    if not self:IsAddonSFXEnabled() then
        return false
    end
    local profile = UI_SOUND_PROFILE[tostring(kind or "")] or UI_SOUND_PROFILE.button
    if type(profile) ~= "table" then
        return false
    end
    for _, spec in ipairs(profile) do
        local soundID = ResolveSoundKitIDFromSpec(spec)
        if soundID and TryPlaySoundKitVariants(soundID, "SFX") then
            return true
        end
    end
    return false
end

function MM:Print(text)
    local msg = tostring(text or "")
    if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
        DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99Murloc Minder:|r " .. msg)
    else
        print("Murloc Minder: " .. msg)
    end
end

local function EnsureCopyBlockFrame(self)
    if self.copyBlockFrame and self.copyBlockFrame.EditBox and self.copyBlockFrame.TitleText then
        return self.copyBlockFrame
    end
    if type(CreateFrame) ~= "function" or not UIParent then
        return nil
    end

    local frame = CreateFrame("Frame", "MurlocMinderCopyBlockFrame", UIParent, "BackdropTemplate")
    frame:SetSize(760, 460)
    frame:SetPoint("CENTER")
    frame:SetFrameStrata("DIALOG")
    frame:SetToplevel(true)
    frame:EnableMouse(true)
    frame:SetMovable(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetClampedToScreen(true)
    frame:SetScript("OnDragStart", function(f)
        f:StartMoving()
    end)
    frame:SetScript("OnDragStop", function(f)
        f:StopMovingOrSizing()
    end)
    frame:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
        tile = true,
        tileSize = 32,
        edgeSize = 32,
        insets = { left = 8, right = 8, top = 8, bottom = 8 },
    })

    local title = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    title:SetPoint("TOPLEFT", frame, "TOPLEFT", 16, -16)
    title:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -44, -16)
    title:SetJustifyH("LEFT")
    title:SetText("Telemetry")
    frame.TitleText = title

    local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -5, -5)
    close:SetScript("OnClick", function()
        frame:Hide()
        if self.PlayUISound then
            self:PlayUISound("popup_close")
        end
    end)

    local scroll = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 18, -44)
    scroll:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -38, 44)

    local edit = CreateFrame("EditBox", nil, scroll)
    edit:SetMultiLine(true)
    edit:SetAutoFocus(false)
    edit:SetFontObject(ChatFontNormal)
    edit:SetWidth(680)
    edit:SetJustifyH("LEFT")
    edit:SetJustifyV("TOP")
    edit:SetTextInsets(2, 2, 2, 2)
    edit:SetScript("OnEscapePressed", function()
        frame:Hide()
        if self.PlayUISound then
            self:PlayUISound("popup_close")
        end
    end)
    edit:SetScript("OnTextChanged", function(box)
        box:GetParent():UpdateScrollChildRect()
    end)
    scroll:SetScrollChild(edit)
    frame.EditBox = edit

    local selectButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    selectButton:SetSize(100, 22)
    selectButton:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -14, 12)
    selectButton:SetText("Select All")
    selectButton:SetScript("OnClick", function()
        edit:SetFocus()
        edit:HighlightText()
    end)

    local closeButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    closeButton:SetSize(90, 22)
    closeButton:SetPoint("RIGHT", selectButton, "LEFT", -8, 0)
    closeButton:SetText("Close")
    closeButton:SetScript("OnClick", function()
        frame:Hide()
        if self.PlayUISound then
            self:PlayUISound("popup_close")
        end
    end)

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

function MM:ShowCopyableTextBlock(title, text)
    local content = tostring(text or "")
    if content == "" then
        self:Print("Nothing to copy.")
        return false
    end

    local frame = EnsureCopyBlockFrame(self)
    if not frame then
        self:Print(content)
        return false
    end

    frame.TitleText:SetText(tostring(title or "Murloc Minder"))
    frame.EditBox:SetText(content)
    frame.EditBox:SetCursorPosition(0)
    frame:Show()
    frame.EditBox:SetFocus()
    frame.EditBox:HighlightText()
    if self.PlayUISound then
        self:PlayUISound("popup_open")
    end
    return true
end

local function EnsureSocialCopyFrame(self)
    if self.socialCopyFrame and self.socialCopyFrame.EditBox and self.socialCopyFrame.TitleText then
        return self.socialCopyFrame
    end
    if type(CreateFrame) ~= "function" or not UIParent then
        return nil
    end

    local frame = CreateFrame("Frame", "MurlocMinderSocialCopyFrame", UIParent, "BackdropTemplate")
    frame:SetSize(560, 118)
    frame:SetPoint("CENTER")
    frame:SetFrameStrata("DIALOG")
    frame:SetToplevel(true)
    frame:SetMovable(true)
    frame:EnableMouse(true)
    frame:SetClampedToScreen(true)
    if frame.SetBackdrop then
        frame:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        frame:SetBackdropColor(0.035, 0.04, 0.055, 0.98)
        frame:SetBackdropBorderColor(0.19, 0.22, 0.28, 1)
    end
    frame:RegisterForDrag("LeftButton")
    frame:SetScript("OnDragStart", function(panel)
        panel:StartMoving()
    end)
    frame:SetScript("OnDragStop", function(panel)
        panel:StopMovingOrSizing()
    end)

    local shadow = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    shadow:SetPoint("TOPLEFT", frame, "TOPLEFT", -8, 8)
    shadow:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 8, -8)
    if shadow.SetBackdrop then
        shadow:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        shadow:SetBackdropColor(0, 0, 0, 0)
        shadow:SetBackdropBorderColor(0, 0, 0, 0.35)
    end
    shadow:SetFrameLevel(math.max(1, frame:GetFrameLevel() - 1))

    local header = CreateFrame("Frame", nil, frame)
    header:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1)
    header:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1)
    header:SetHeight(40)
    header:EnableMouse(true)
    header:RegisterForDrag("LeftButton")
    header:SetScript("OnDragStart", function()
        frame:StartMoving()
    end)
    header:SetScript("OnDragStop", function()
        frame:StopMovingOrSizing()
    end)

    local headerBg = header:CreateTexture(nil, "BACKGROUND")
    headerBg:SetAllPoints()
    headerBg:SetColorTexture(0.055, 0.065, 0.09, 1)

    local headerAccent = header:CreateTexture(nil, "ARTWORK")
    headerAccent:SetPoint("BOTTOMLEFT", header, "BOTTOMLEFT", 0, 0)
    headerAccent:SetPoint("BOTTOMRIGHT", header, "BOTTOMRIGHT", 0, 0)
    headerAccent:SetHeight(2)
    headerAccent:SetTexture("Interface\\Buttons\\WHITE8x8")
    if headerAccent.SetGradient and CreateColor then
        headerAccent:SetGradient("HORIZONTAL", CreateColor(0.97, 0.03, 0.57, 1), CreateColor(0.08, 0.39, 0.98, 1))
    else
        headerAccent:SetColorTexture(0.97, 0.03, 0.57, 1)
    end

    local title = header:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("LEFT", header, "LEFT", 14, 0)
    title:SetTextColor(1, 1, 1)
    title:SetText("Link")
    frame.TitleText = title

    local closeButton = CreateFrame("Button", nil, header)
    closeButton:SetSize(26, 26)
    closeButton:SetPoint("RIGHT", header, "RIGHT", -10, 0)
    local closeBg = closeButton:CreateTexture(nil, "BACKGROUND")
    closeBg:SetAllPoints()
    closeBg:SetColorTexture(0.12, 0.14, 0.18, 1)
    local closeHighlight = closeButton:CreateTexture(nil, "HIGHLIGHT")
    closeHighlight:SetAllPoints()
    closeHighlight:SetColorTexture(1, 1, 1, 0.08)
    local closeText = closeButton:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    closeText:SetPoint("CENTER")
    closeText:SetTextColor(0.95, 0.95, 0.95)
    closeText:SetText("x")
    closeButton:SetScript("OnClick", function()
        frame:Hide()
        if self.PlayUISound then
            self:PlayUISound("popup_close")
        end
    end)
    frame.CloseButton = closeButton

    local content = CreateFrame("Frame", nil, frame)
    content:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -54)
    content:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -14, 14)

    local iconFrame = CreateFrame("Frame", nil, content, "BackdropTemplate")
    iconFrame:SetSize(52, 52)
    iconFrame:SetPoint("LEFT", content, "LEFT", 0, 0)
    if iconFrame.SetBackdrop then
        iconFrame:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        iconFrame:SetBackdropColor(0.07, 0.08, 0.11, 1)
        iconFrame:SetBackdropBorderColor(0.19, 0.22, 0.28, 1)
    end
    frame.IconFrame = iconFrame

    local iconAccent = iconFrame:CreateTexture(nil, "ARTWORK")
    iconAccent:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", 1, -1)
    iconAccent:SetPoint("TOPRIGHT", iconFrame, "TOPRIGHT", -1, -1)
    iconAccent:SetHeight(2)
    iconAccent:SetTexture("Interface\\Buttons\\WHITE8x8")
    frame.IconAccent = iconAccent

    local iconTexture = iconFrame:CreateTexture(nil, "ARTWORK")
    iconTexture:SetSize(28, 28)
    iconTexture:SetPoint("CENTER")
    frame.IconTexture = iconTexture

    local iconBadge = iconFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    iconBadge:SetPoint("CENTER")
    iconBadge:SetTextColor(1, 1, 1)
    frame.IconBadge = iconBadge

    local field = CreateFrame("Frame", nil, content, "BackdropTemplate")
    field:SetPoint("LEFT", iconFrame, "RIGHT", 12, 0)
    field:SetPoint("RIGHT", content, "RIGHT", 0, 0)
    field:SetHeight(40)
    if field.SetBackdrop then
        field:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        field:SetBackdropColor(0.07, 0.08, 0.11, 1)
        field:SetBackdropBorderColor(0.19, 0.22, 0.28, 1)
    end

    local edit = CreateFrame("EditBox", nil, field)
    edit:SetMultiLine(false)
    edit:SetAutoFocus(false)
    edit:SetFontObject(ChatFontNormal)
    edit:SetPoint("TOPLEFT", field, "TOPLEFT", 10, -11)
    edit:SetPoint("BOTTOMRIGHT", field, "BOTTOMRIGHT", -10, 11)
    edit:SetJustifyH("LEFT")
    edit:SetJustifyV("MIDDLE")
    edit:SetTextInsets(0, 0, 0, 0)
    edit:SetScript("OnEscapePressed", function()
        frame:Hide()
        if self.PlayUISound then
            self:PlayUISound("popup_close")
        end
    end)
    edit:SetScript("OnMouseDown", function(box)
        box:SetFocus()
        box:HighlightText()
    end)
    frame.EditBox = edit

    if type(UISpecialFrames) == "table" then
        local exists = false
        for _, name in ipairs(UISpecialFrames) do
            if name == frame:GetName() then
                exists = true
                break
            end
        end
        if not exists then
            UISpecialFrames[#UISpecialFrames + 1] = frame:GetName()
        end
    end

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

function MM:ShowCopyableSocialLink(entry)
    if type(entry) ~= "table" then
        return self:ShowCopyableTextBlock("Link", "")
    end

    local url = tostring(entry.url or "")
    if url == "" then
        self:Print("Nothing to copy.")
        return false
    end

    local frame = EnsureSocialCopyFrame(self)
    if not frame then
        self:Print(url)
        return false
    end

    frame.TitleText:SetText(tostring(entry.label or "Link"))
    frame.EditBox:SetText(url)
    frame.EditBox:SetCursorPosition(0)

    local accent = entry.accent or { 0.19, 0.22, 0.28 }
    if frame.IconAccent then
        frame.IconAccent:SetTexture("Interface\\Buttons\\WHITE8x8")
        if frame.IconAccent.SetGradient and CreateColor then
            frame.IconAccent:SetGradient("HORIZONTAL", CreateColor(accent[1], accent[2], accent[3], 1), CreateColor(0.08, 0.39, 0.98, 1))
        else
            frame.IconAccent:SetColorTexture(accent[1], accent[2], accent[3], 1)
        end
    end
    if frame.IconTexture then
        if entry.icon then
            frame.IconTexture:SetTexture(entry.icon)
            frame.IconTexture:Show()
            frame.IconBadge:Hide()
        else
            frame.IconTexture:Hide()
            frame.IconBadge:SetText(tostring(entry.badge or "?"))
            frame.IconBadge:Show()
        end
    end

    frame:Show()
    frame:Raise()
    frame.EditBox:SetFocus()
    frame.EditBox:HighlightText()
    if self.PlayUISound then
        self:PlayUISound("popup_open")
    end
    return true
end

function MM:GetAddonVersionText()
    local function normalize(version)
        local text = tostring(version or "")
        local major, minor, patch = text:match("(%d+)%.(%d+)%.(%d+)")
        if major then
            return string.format("%s.%s.%s", major, minor, patch)
        end
        return ""
    end

    local version = ""
    if type(GetAddOnMetadata) == "function" then
        version = normalize(GetAddOnMetadata(ADDON_NAME, "Version"))
        if version == "" then
            version = normalize(GetAddOnMetadata("MurlocMinder", "Version"))
        end
    end
    if version == "" and C_AddOns and type(C_AddOns.GetAddOnMetadata) == "function" then
        version = normalize(C_AddOns.GetAddOnMetadata(ADDON_NAME, "Version"))
        if version == "" then
            version = normalize(C_AddOns.GetAddOnMetadata("MurlocMinder", "Version"))
        end
    end
    if version == "" and self and self.frame and self.frame.TitleText and self.frame.TitleText.GetText then
        version = normalize((self.frame.TitleText:GetText() or ""):match("v(%d+%.%d+%.%d+)"))
    end
    if version == "" then
        version = normalize(TITLE_VERSION_FALLBACK)
    end
    if version == "" then
        version = "0.1.7"
    end
    return version
end

local function GetTooltipPercent(value)
    return math.floor((tonumber(value) or 0) + 0.5)
end

local function GetTooltipColor(key)
    local color = MINIMAP_TOOLTIP_COLORS[key] or { 1, 1, 1 }
    return color[1], color[2], color[3]
end

local function GetPolarAngleDegrees(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

function MM:ShowTelemetryReport(chatNotice)
    if not self.BuildDrainTelemetryReport then
        self:Print("Telemetry report is unavailable.")
        return false
    end

    local report = self:BuildDrainTelemetryReport()
    if self:ShowCopyableTextBlock("Murloc Minder Drain Telemetry", report) then
        if chatNotice then
            self:Print("Telemetry copied to popup. Use Select All, then Ctrl+C.")
        end
        return true
    end

    self:Print("Telemetry report:")
    self:Print(report)
    return false
end

function MM:UpdateMinimapButtonPosition()
    local button = self.minimapButton
    if not button or not Minimap or not Minimap.GetCenter then
        return
    end

    self.db.profile.minimap = self.db.profile.minimap or {}
    local angle = tonumber(self.db.profile.minimap.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 MM:UpdateMinimapButtonDrag()
    if not Minimap or not Minimap.GetCenter or not GetCursorPosition then
        return
    end

    local scale = Minimap:GetEffectiveScale() or 1
    local cursorX, cursorY = GetCursorPosition()
    local centerX, centerY = Minimap:GetCenter()
    if not cursorX or not cursorY or not centerX or not centerY then
        return
    end

    local dx = (cursorX / scale) - centerX
    local dy = (cursorY / scale) - centerY

    self.db.profile.minimap = self.db.profile.minimap or {}
    self.db.profile.minimap.angle = GetPolarAngleDegrees(dy, dx)
    self:UpdateMinimapButtonPosition()
end

function MM:UpdateMinimapButtonVisibility()
    self.db.profile.minimap = self.db.profile.minimap or {}

    local button = self:EnsureMinimapButton()
    if not button then
        return
    end

    local visible = self.db.profile.minimap.hide ~= true
    button:SetShown(visible)
    if visible then
        self:UpdateMinimapButtonPosition()
    end
end

function MM:HideMinimapContextMenu()
    if self.minimapMenuFrame then
        self.minimapMenuFrame:Hide()
    end
    if self.minimapMenuDismissFrame then
        self.minimapMenuDismissFrame:Hide()
    end
end

function MM:ShowMinimapContextMenu(owner)
    if type(CreateFrame) ~= "function" then
        return
    end

    self.db.profile.ui = self.db.profile.ui or {}
    if not self.minimapMenuDismissFrame then
        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(_, mouseButton)
            if mouseButton == "LeftButton" then
                self:HideMinimapContextMenu()
            end
        end)
        dismiss:Hide()
        self.minimapMenuDismissFrame = dismiss
    end

    if not self.minimapMenuFrame then
        local frame = CreateFrame("Frame", "MurlocMinderMinimapContextMenu", UIParent, "BackdropTemplate")
        frame:SetSize(172, 128)
        frame:SetFrameStrata("DIALOG")
        frame:SetFrameLevel(10)
        frame:SetClampedToScreen(true)
        if frame.SetBackdrop then
            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(0.03, 0.04, 0.06, 0.96)
            frame:SetBackdropBorderColor(0.85, 0.7, 0.18, 1)
        end

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

        local divider = frame:CreateTexture(nil, "ARTWORK")
        divider:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -30)
        divider:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -8, -30)
        divider:SetHeight(1)
        divider:SetColorTexture(0.24, 0.61, 0.78, 0.55)

        local function createMenuButton(label, yOffset, onClick)
            local button = CreateFrame("Button", nil, frame)
            button:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, yOffset)
            button:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -8, yOffset)
            button:SetHeight(24)

            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:SetJustifyH("LEFT")
            text:SetTextColor(0.93, 0.91, 0.82)
            text:SetText(label)
            button.Text = text

            button:SetScript("OnClick", function()
                self:HideMinimapContextMenu()
                onClick()
            end)
            return button
        end

        frame.ToggleTrackerButton = createMenuButton("Toggle tracker", -38, function()
            self.db.profile.ui = self.db.profile.ui or {}
            self.db.profile.ui.showTracker = not (self.db.profile.ui.showTracker ~= false)
            if self.RefreshUI then
                self:RefreshUI(true)
            end
        end)
        frame.TelemetryButton = createMenuButton("Telemetry", -64, function()
            self:ShowTelemetryReport(false)
        end)
        frame.DeveloperButton = createMenuButton("Developer Info", -90, function()
            self:ShowDeveloperInfoPanel()
        end)

        frame:Hide()
        self.minimapMenuFrame = frame
    end

    local frame = self.minimapMenuFrame
    if frame:IsShown() then
        self:HideMinimapContextMenu()
        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 cursorX, cursorY = GetCursorPosition()
        local x = (cursorX or 0) / scale
        local y = (cursorY or 0) / scale
        frame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x + 8, y - 8)
    end
    self.minimapMenuDismissFrame:Show()
    frame:Show()
end

function MM:EnsureMinimapButton()
    if self.minimapButton then
        return self.minimapButton
    end
    if type(CreateFrame) ~= "function" or type(Minimap) ~= "table" then
        return nil
    end

    local button = CreateFrame("Button", "MurlocMinderMinimapButton", 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(ICON_MURLOC)
    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(ICON_MURLOC)
    pushed:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    button:SetPushedTexture(pushed)

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

    button:SetScript("OnClick", function(_, mouseButton)
        if mouseButton == "RightButton" then
            self:ShowMinimapContextMenu(button)
            return
        end
        if mouseButton ~= "LeftButton" then
            return
        end
        if self.ToggleMainFrame then
            self:ToggleMainFrame()
        end
    end)
    button:SetScript("OnDragStart", function(btn)
        btn:SetScript("OnUpdate", function()
            self:UpdateMinimapButtonDrag()
        end)
    end)
    button:SetScript("OnDragStop", function(btn)
        btn:SetScript("OnUpdate", nil)
        self:UpdateMinimapButtonDrag()
    end)
    button:SetScript("OnHide", function(btn)
        btn:SetScript("OnUpdate", nil)
    end)
    button:SetScript("OnEnter", function(btn)
        local pet = (self.GetPet and self:GetPet()) or (self.db and self.db.profile and self.db.profile.pet) or {}
        local petName = ""
        if type(self.GetPetDisplayName) == "function" then
            petName = tostring(self:GetPetDisplayName() or "")
        else
            petName = TrimString(tostring(pet.name or ""))
        end

        GameTooltip:SetOwner(btn, "ANCHOR_LEFT")
        GameTooltip:SetText("Murloc Minder")
        if petName ~= "" then
            GameTooltip:AddLine(petName, 1.0, 0.82, 0.1)
        end
        GameTooltip:AddLine("Left Click: Open main panel", 1, 1, 1)
        GameTooltip:AddLine("Right Click: Options", 1, 1, 1)
        GameTooltip:AddLine("Drag: Move along minimap", 0.65, 0.85, 1.0)
        GameTooltip:AddLine(" ")

        if pet and pet.stage == "egg" then
            local hatchR, hatchG, hatchB = GetTooltipColor("hatch")
            GameTooltip:AddDoubleLine("Hatch Progress", string.format("%d%%", GetTooltipPercent(pet.hatchProgress)), 1, 1, 1, hatchR, hatchG, hatchB)
        else
            local hungerR, hungerG, hungerB = GetTooltipColor("hunger")
            local joyR, joyG, joyB = GetTooltipColor("joy")
            local energyR, energyG, energyB = GetTooltipColor("energy")
            local cleanR, cleanG, cleanB = GetTooltipColor("cleanliness")
            GameTooltip:AddDoubleLine("Hunger", string.format("%d%%", GetTooltipPercent(pet.hunger)), 1, 1, 1, hungerR, hungerG, hungerB)
            GameTooltip:AddDoubleLine("Joy", string.format("%d%%", GetTooltipPercent(pet.joy)), 1, 1, 1, joyR, joyG, joyB)
            GameTooltip:AddDoubleLine("Energy", string.format("%d%%", GetTooltipPercent(pet.energy)), 1, 1, 1, energyR, energyG, energyB)
            GameTooltip:AddDoubleLine("Cleanliness", string.format("%d%%", GetTooltipPercent(pet.cleanliness)), 1, 1, 1, cleanR, cleanG, cleanB)
        end
        GameTooltip:Show()
    end)
    button:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    self.minimapButton = button
    self:UpdateMinimapButtonPosition()
    return button
end

local function EnsureDeveloperInfoFrame(self)
    if self.developerInfoFrame then
        return self.developerInfoFrame
    end
    if type(CreateFrame) ~= "function" then
        return nil
    end

    local frame = CreateFrame("Frame", "MurlocMinderDeveloperInfoFrame", UIParent, "BackdropTemplate")
    frame:SetSize(720, 472)
    frame:SetPoint("CENTER")
    frame:SetFrameStrata("DIALOG")
    frame:SetClampedToScreen(true)
    frame:SetMovable(true)
    frame:EnableMouse(true)
    if frame.SetBackdrop then
        frame:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        frame:SetBackdropColor(0.035, 0.04, 0.055, 0.98)
        frame:SetBackdropBorderColor(0.19, 0.22, 0.28, 1)
    end

    local shadow = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    shadow:SetPoint("TOPLEFT", frame, "TOPLEFT", -8, 8)
    shadow:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 8, -8)
    if shadow.SetBackdrop then
        shadow:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        shadow:SetBackdropColor(0, 0, 0, 0)
        shadow:SetBackdropBorderColor(0, 0, 0, 0.35)
    end
    shadow:SetFrameLevel(math.max(1, frame:GetFrameLevel() - 1))
    frame.Shadow = shadow

    local header = CreateFrame("Frame", nil, frame)
    header:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1)
    header:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1)
    header:SetHeight(44)
    header:EnableMouse(true)
    header:RegisterForDrag("LeftButton")
    header:SetScript("OnDragStart", function()
        frame:StartMoving()
    end)
    header:SetScript("OnDragStop", function()
        frame:StopMovingOrSizing()
    end)

    local headerBg = header:CreateTexture(nil, "BACKGROUND")
    headerBg:SetAllPoints()
    headerBg:SetColorTexture(0.055, 0.065, 0.09, 1)

    local headerAccent = header:CreateTexture(nil, "ARTWORK")
    headerAccent:SetPoint("BOTTOMLEFT", header, "BOTTOMLEFT", 0, 0)
    headerAccent:SetPoint("BOTTOMRIGHT", header, "BOTTOMRIGHT", 0, 0)
    headerAccent:SetHeight(2)
    headerAccent:SetTexture("Interface\\Buttons\\WHITE8x8")
    if headerAccent.SetGradient and CreateColor then
        headerAccent:SetGradient("HORIZONTAL", CreateColor(0.97, 0.03, 0.57, 1), CreateColor(0.08, 0.39, 0.98, 1))
    else
        headerAccent:SetColorTexture(0.97, 0.03, 0.57, 1)
    end

    local title = header:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("LEFT", header, "LEFT", 16, 0)
    title:SetTextColor(1, 1, 1)
    title:SetText("Developer Info")
    frame.TitleText = title

    local closeButton = CreateFrame("Button", nil, header)
    closeButton:SetSize(26, 26)
    closeButton:SetPoint("RIGHT", header, "RIGHT", -10, 0)

    local closeBg = closeButton:CreateTexture(nil, "BACKGROUND")
    closeBg:SetAllPoints()
    closeBg:SetColorTexture(0.12, 0.14, 0.18, 1)
    local closeHighlight = closeButton:CreateTexture(nil, "HIGHLIGHT")
    closeHighlight:SetAllPoints()
    closeHighlight:SetColorTexture(1, 1, 1, 0.08)
    local closeText = closeButton:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    closeText:SetPoint("CENTER", 0, 0)
    closeText:SetTextColor(0.95, 0.95, 0.95)
    closeText:SetText("x")
    closeButton:SetScript("OnClick", function()
        frame:Hide()
        if self.PlayUISound then
            self:PlayUISound("popup_close")
        end
    end)
    frame.CloseButton = closeButton

    if type(UISpecialFrames) == "table" then
        local exists = false
        for _, name in ipairs(UISpecialFrames) do
            if name == frame:GetName() then
                exists = true
                break
            end
        end
        if not exists then
            UISpecialFrames[#UISpecialFrames + 1] = frame:GetName()
        end
    end

    local content = CreateFrame("Frame", nil, frame)
    content:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -56)
    content:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -14, 14)
    frame.Content = content

    local heroPanel = CreateFrame("Frame", nil, content)
    heroPanel:SetSize(224, 390)
    heroPanel:SetPoint("TOPLEFT", content, "TOPLEFT", 14, -4)
    local heroBg = heroPanel:CreateTexture(nil, "BACKGROUND")
    heroBg:SetAllPoints()
    heroBg:SetColorTexture(0.06, 0.07, 0.09, 0.95)

    local heroAccent = heroPanel:CreateTexture(nil, "BORDER")
    heroAccent:SetPoint("TOPLEFT", heroPanel, "TOPLEFT", 0, 0)
    heroAccent:SetPoint("TOPRIGHT", heroPanel, "TOPRIGHT", 0, 0)
    heroAccent:SetHeight(3)
    heroAccent:SetTexture("Interface\\Buttons\\WHITE8x8")
    if heroAccent.SetGradient and CreateColor then
        heroAccent:SetGradient("HORIZONTAL", CreateColor(0.97, 0.03, 0.57, 1), CreateColor(0.08, 0.39, 0.98, 1))
    else
        heroAccent:SetColorTexture(0.97, 0.03, 0.57, 1)
    end

    local portrait = heroPanel:CreateTexture(nil, "ARTWORK")
    portrait:SetSize(176, 176)
    portrait:SetPoint("TOP", heroPanel, "TOP", 0, -18)
    portrait:SetTexture(SOCIAL_MEDIA_ROOT .. "kagrok_full.png")
    portrait:SetTexCoord(0.04, 0.96, 0.04, 0.96)

    local nameText = heroPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    nameText:SetPoint("TOPLEFT", portrait, "BOTTOMLEFT", 0, -14)
    nameText:SetPoint("TOPRIGHT", portrait, "BOTTOMRIGHT", 0, -14)
    nameText:SetJustifyH("CENTER")
    nameText:SetTextColor(1, 1, 1)
    nameText:SetText("Kagrok")

    local titleText = heroPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    titleText:SetPoint("TOPLEFT", nameText, "BOTTOMLEFT", 0, -6)
    titleText:SetPoint("TOPRIGHT", nameText, "BOTTOMRIGHT", 0, -6)
    titleText:SetJustifyH("CENTER")
    titleText:SetTextColor(0.78, 0.83, 0.9)
    titleText:SetText("WoW Addon Developer")

    local bodyText = heroPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    bodyText:SetPoint("TOPLEFT", titleText, "BOTTOMLEFT", 4, -20)
    bodyText:SetPoint("TOPRIGHT", titleText, "BOTTOMRIGHT", -4, -20)
    bodyText:SetJustifyH("LEFT")
    bodyText:SetJustifyV("TOP")
    bodyText:SetTextColor(0.86, 0.88, 0.92)
    bodyText:SetText("Murloc Minder and other World of Warcraft projects.\n\nClick any platform on the right to open a copyable link.")

    local linksPanel = CreateFrame("Frame", nil, content)
    linksPanel:SetPoint("TOPLEFT", heroPanel, "TOPRIGHT", 22, -10)
    linksPanel:SetPoint("BOTTOMRIGHT", content, "BOTTOMRIGHT", -16, 14)

    frame.LinkButtons = {}
    for index, entry in ipairs(SOCIAL_LINK_ENTRIES) do
        local row = CreateFrame("Button", nil, linksPanel)
        row:SetPoint("TOPLEFT", linksPanel, "TOPLEFT", 0, -((index - 1) * 52))
        row:SetPoint("TOPRIGHT", linksPanel, "TOPRIGHT", 0, -((index - 1) * 52))
        row:SetHeight(44)

        local accent = row:CreateTexture(nil, "BORDER")
        accent:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
        accent:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
        accent:SetWidth(4)
        accent:SetColorTexture(entry.accent[1], entry.accent[2], entry.accent[3], 1)

        local bg = row:CreateTexture(nil, "BACKGROUND")
        bg:SetPoint("TOPLEFT", row, "TOPLEFT", 4, 0)
        bg:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
        bg:SetColorTexture(0.07, 0.08, 0.11, 0.96)

        local highlight = row:CreateTexture(nil, "HIGHLIGHT")
        highlight:SetPoint("TOPLEFT", row, "TOPLEFT", 4, 0)
        highlight:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
        highlight:SetColorTexture(1, 1, 1, 0.05)

        local iconFrame = CreateFrame("Frame", nil, row)
        iconFrame:SetSize(28, 28)
        iconFrame:SetPoint("LEFT", row, "LEFT", 16, 0)
        local iconBg = iconFrame:CreateTexture(nil, "BACKGROUND")
        iconBg:SetAllPoints()
        iconBg:SetColorTexture(0.12, 0.14, 0.18, 1)

        if entry.icon then
            local icon = iconFrame:CreateTexture(nil, "ARTWORK")
            icon:SetAllPoints()
            icon:SetTexture(entry.icon)
        else
            local badge = iconFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
            badge:SetPoint("CENTER")
            badge:SetTextColor(1, 1, 1)
            badge:SetText(entry.badge or "?")
        end

        local label = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        label:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 14, -7)
        label:SetTextColor(1, 1, 1)
        label:SetText(entry.label)

        local info = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        info:SetPoint("TOPLEFT", label, "BOTTOMLEFT", 0, -4)
        info:SetPoint("RIGHT", row, "RIGHT", -14, 0)
        info:SetJustifyH("LEFT")
        info:SetTextColor(0.74, 0.79, 0.86)
        info:SetText(entry.tooltip)

        row:SetScript("OnClick", function()
            self:ShowCopyableSocialLink(entry)
        end)
        row:SetScript("OnEnter", function(btn)
            GameTooltip:SetOwner(btn, "ANCHOR_RIGHT")
            GameTooltip:SetText(entry.label, 1, 1, 1)
            GameTooltip:AddLine(entry.tooltip, 0.84, 0.88, 0.94, true)
            GameTooltip:AddLine("Click to copy link", 1.0, 0.82, 0.1)
            GameTooltip:Show()
        end)
        row:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)

        frame.LinkButtons[#frame.LinkButtons + 1] = row
    end

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

function MM:ShowDeveloperInfoPanel()
    local frame = EnsureDeveloperInfoFrame(self)
    if not frame then
        return
    end

    frame:Show()
    frame:Raise()
    if self.PlayUISound then
        self:PlayUISound("popup_open")
    end
end

function MM:RegisterChatCommand(command, handler)
    local token = "MURLOCMINDER_" .. command:gsub("[^%w]", ""):upper()
    local slashKey = "SLASH_" .. token .. "1"
    _G[slashKey] = "/" .. command

    SlashCmdList[token] = function(msg)
        if type(handler) == "function" then
            handler(self, msg)
            return
        end

        if type(handler) == "string" and type(self[handler]) == "function" then
            self[handler](self, msg)
        end
    end
end

function MM:ScheduleRepeatingTimer(method, interval)
    local callback
    if type(method) == "string" then
        callback = function()
            if type(self[method]) == "function" then
                self[method](self)
            end
        end
    else
        callback = function()
            method(self)
        end
    end

    return C_Timer.NewTicker(interval, callback)
end

function MM:CancelTimer(timer)
    if timer and timer.Cancel then
        timer:Cancel()
    end
end

function MM:ResetProfile()
    MurlocMinderDB = DeepCopy(defaults)
    self.db = MurlocMinderDB
end

function MM:OnInitialize()
    if type(MurlocMinderDB) ~= "table" then
        MurlocMinderDB = DeepCopy(defaults)
    else
        MergeDefaults(MurlocMinderDB, defaults)
    end

    self.db = MurlocMinderDB

    self:RegisterChatCommand("murlocminder", "HandleSlash")
    self:RegisterChatCommand("mm", "HandleSlash")
    self:RegisterChatCommand("mminder", "HandleSlash")
    self:RegisterChatCommand("murloc", "HandleSlash")
    self:RegisterChatCommand("mmdev", "HandleDevSlash")

    self:InitializePetState()
    self:InitializeUI()
    if self.db and self.db.profile and self.db.profile.welcomeSeen ~= true and self.ShowMainFrame then
        self:ShowMainFrame()
    end
    self:SetupLauncher()
end

function MM:HandleDevSlash()
    self.db.profile.dev = self.db.profile.dev or {}
    local enabled = not (self.db.profile.dev.showDevTab == true)
    self.db.profile.dev.showDevTab = enabled

    if self.UpdateDevTabVisibility then
        self:UpdateDevTabVisibility()
    end
    if self.RefreshUI then
        self:RefreshUI(true)
    end

    self:Print(enabled and "Dev tab enabled." or "Dev tab hidden.")
end

function MM:OnEnable()
    self:StartNeedsDecay()
end

function MM:OnDisable()
    if self.decayTimer then
        self:CancelTimer(self.decayTimer)
        self.decayTimer = nil
    end

    if self.hopTimer then
        self:CancelTimer(self.hopTimer)
        self.hopTimer = nil
    end
end

function MM:HandleSlash(input)
    local cmd = TrimString((input or ""):lower())
    local primary, rest = cmd:match("^(%S+)%s*(.-)$")
    primary = primary or ""
    rest = rest or ""

    if cmd == "" or cmd == "open" or cmd == "show" then
        self:ShowMainFrame()
        return
    end

    if cmd == "toggle" then
        self:ToggleMainFrame()
        return
    end

    if cmd == "hide" then
        self:HideMainFrame()
        return
    end

    if cmd == "reset" then
        self:ResetProfile()
        self:InitializePetState(true)
        self:ResetMainFramePosition()
        self:RefreshUI(true)
        self:Print("Murloc Minder profile reset.")
        return
    end

    if primary == "creset" then
        local slot = tonumber((rest or ""):match("^(%d+)"))
        if not slot or slot < 1 or slot > 4 then
            self:Print("Usage: /mm creset <1-4>")
            return
        end
        if self.DevCompleteChallengeSlot then
            local ok, msg = self:DevCompleteChallengeSlot(slot)
            if msg and msg ~= "" then
                self:Print(msg)
            end
            if ok and self.RefreshUI then
                self:RefreshUI(true)
            end
            return
        end
    end

    if primary == "telemetry" or primary == "telem" then
        self:ShowTelemetryReport(true)
        return
    end

    if primary == "devinfo" then
        self:ShowDeveloperInfoPanel()
        return
    end

    self:Print("Commands: /murlocminder open|hide|toggle|reset|creset <1-4>|telemetry|devinfo")
end

function MM:SetupLauncher()
    self:EnsureMinimapButton()
    self:UpdateMinimapButtonVisibility()

    if type(LibStub) ~= "function" then
        return
    end

    local ldb = LibStub("LibDataBroker-1.1", true)
    local icon = LibStub("LibDBIcon-1.0", true)
    if ldb and not self.dataObject then
        self.dataObject = ldb:NewDataObject("MurlocMinder", {
            type = "launcher",
            text = "Murloc Minder",
            icon = ICON_MURLOC,
            OnClick = function(_, button)
                if button == "RightButton" then
                    self:ShowMinimapContextMenu(Minimap or UIParent)
                    return
                end
                if self.ShowMainFrame then
                    self:ShowMainFrame()
                end
            end,
            OnTooltipShow = function(tooltip)
                tooltip:AddLine("Murloc Minder")
                tooltip:AddLine("Left Click: Open main panel", 1, 1, 1)
                tooltip:AddLine("Right Click: Options", 1, 1, 1)
                tooltip:AddLine("Slash: /murlocminder", 0.6, 0.85, 1)
            end,
        })
    end

    if icon and icon.Hide then
        pcall(function()
            icon:Hide("MurlocMinder")
        end)
    end
end

local function NormalizeActivityMeta(meta)
    if type(meta) == "table" then
        local source = type(meta.source) == "string" and TrimString(meta.source) or ""
        local tone = type(meta.tone) == "string" and TrimString(meta.tone) or ""
        local need = type(meta.need) == "string" and TrimString(meta.need) or ""
        if source == "" and meta.benign == true then
            source = "autonomous"
        end
        return {
            source = source ~= "" and source or nil,
            tone = tone ~= "" and tone or nil,
            need = need ~= "" and need or nil,
        }
    end
    if meta == true then
        return { source = "autonomous" }
    end
    if type(meta) == "string" then
        local source = TrimString(meta)
        if source ~= "" then
            return { source = source }
        end
    end
    return {}
end

local function CollapseAdjacentDuplicateWords(text)
    local out = tostring(text or "")
    if out == "" then
        return out
    end

    local passes = 0
    while passes < 8 do
        local changed = false
        out = out:gsub("([%a][%a']*)(%s+)([%a][%a']*)", function(a, sep, b)
            if string.lower(a) == string.lower(b) then
                changed = true
                return a
            end
            return a .. sep .. b
        end)
        if not changed then
            break
        end
        passes = passes + 1
    end
    return out
end

function MM:GetActivityEntryLine(entry)
    if type(entry) == "table" then
        local line = TrimString(tostring(entry.line or ""))
        if line ~= "" then
            return line
        end

        local timestamp = TrimString(tostring(entry.timestamp or ""))
        local message = TrimString(tostring(entry.message or ""))
        if message ~= "" then
            if timestamp ~= "" then
                return string.format("[%s] %s", timestamp, message)
            end
            return message
        end
        return ""
    end

    if entry == nil then
        return ""
    end
    return tostring(entry)
end

function MM:PushActivity(text, meta)
    local resolvedMeta = NormalizeActivityMeta(meta)
    local message = TrimString(tostring(text or ""))
    if (resolvedMeta.source == "autonomous" or resolvedMeta.source == "benign") and message ~= "" then
        message = CollapseAdjacentDuplicateWords(message)
    end
    if message == "" then
        return
    end

    self.db.profile.activityLog = self.db.profile.activityLog or {}
    self.db.profile.activityHistory = self.db.profile.activityHistory or {}
    local log = self.db.profile.activityLog
    local history = self.db.profile.activityHistory
    local timestamp = date("%H:%M")
    local line = string.format("[%s] %s", timestamp, message)
    local entry = {
        line = line,
        message = message,
        timestamp = timestamp,
        source = resolvedMeta.source,
        tone = resolvedMeta.tone,
        need = resolvedMeta.need,
    }

    table.insert(log, entry)
    table.insert(history, entry)

    while #log > 10 do
        table.remove(log, 1)
    end
    while #history > 5000 do
        table.remove(history, 1)
    end

    self:RefreshUI()
end

function MM:GetActivityText()
    local log = self.db.profile.activityLog or {}
    if #log == 0 then
        return "No activity yet. Hatch your first murloc egg."
    end

    local lines = {}
    for _, entry in ipairs(log) do
        local line = self:GetActivityEntryLine(entry)
        if line ~= "" then
            lines[#lines + 1] = line
        end
    end
    return table.concat(lines, "\n")
end

function MM:Clamp(value, minV, maxV)
    if value < minV then
        return minV
    end
    if value > maxV then
        return maxV
    end
    return value
end

function MurlocMinder_AddonCompartmentOnClick()
    local addon = _G.MurlocMinder
    if addon and addon.ToggleMainFrame then
        addon:ToggleMainFrame()
    end
end

function MurlocMinder_AddonCompartmentOnEnter(owner)
    local tooltipOwner = owner
    if type(tooltipOwner) ~= "table" or not tooltipOwner.IsObjectType then
        tooltipOwner = UIParent
    end
    GameTooltip:SetOwner(tooltipOwner, "ANCHOR_LEFT")
    GameTooltip:SetText("Murloc Minder")
    GameTooltip:AddLine("Open your murloc habitat", 1, 1, 1)
    GameTooltip:AddLine("Slash: /murlocminder", 0.6, 0.85, 1)
    GameTooltip:Show()
end

function MurlocMinder_AddonCompartmentOnLeave()
    GameTooltip:Hide()
end

local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("ADDON_LOADED")
eventFrame:RegisterEvent("PLAYER_LOGIN")
eventFrame:RegisterEvent("PLAYER_LOGOUT")

eventFrame:SetScript("OnEvent", function(_, event, arg1)
    if event == "ADDON_LOADED" and arg1 == addonName then
        MM:OnInitialize()
    elseif event == "PLAYER_LOGIN" then
        MM:OnEnable()
    elseif event == "PLAYER_LOGOUT" then
        MM:OnDisable()
    end
end)
