local addonName, RF = ...

-------------------------------------------------------------------------------
-- Namespace & Global Access
-------------------------------------------------------------------------------
RosterForge = RF
RF.name = addonName

-------------------------------------------------------------------------------
-- Event Frame
-------------------------------------------------------------------------------
local eventFrame = CreateFrame("Frame")
RF.eventFrame = eventFrame

-------------------------------------------------------------------------------
-- Callback System
-------------------------------------------------------------------------------
local callbacks = {}

function RF:RegisterCallback(event, func)
    if not callbacks[event] then
        callbacks[event] = {}
    end
    callbacks[event][func] = true
end

function RF:UnregisterCallback(event, func)
    if callbacks[event] then
        callbacks[event][func] = nil
    end
end

function RF:FireCallbacks(event, ...)
    if callbacks[event] then
        for func in pairs(callbacks[event]) do
            func(event, ...)
        end
    end
end

-------------------------------------------------------------------------------
-- SavedVariables: MergeDefaults Pattern
-------------------------------------------------------------------------------
local function MergeDefaults(target, defaults)
    for k, v in pairs(defaults) do
        if type(v) == "table" then
            if target[k] == nil then
                target[k] = {}
            end
            if type(target[k]) == "table" then
                MergeDefaults(target[k], v)
            end
        elseif target[k] == nil then
            target[k] = v
        end
    end
end

function RF:InitDB()
    if type(RosterForgeDB) ~= "table" then RosterForgeDB = {} end
    if RosterForgeDB.chars and type(RosterForgeDB.chars) ~= "table" then RosterForgeDB.chars = {} end
    MergeDefaults(RosterForgeDB, { global = RF.DB_DEFAULTS.global })

    local charKey = UnitName("player") .. "-" .. GetRealmName()
    if not RosterForgeDB.chars then
        RosterForgeDB.chars = {}
    end
    if not RosterForgeDB.chars[charKey] then
        RosterForgeDB.chars[charKey] = {}
    end
    MergeDefaults(RosterForgeDB.chars[charKey], RF.DB_DEFAULTS.char)

    RF.db = RosterForgeDB
    RF.charDB = RosterForgeDB.chars[charKey]

    -- Load max teams from saved setting
    if RF.charDB.maxTeams then
        RF.MAX_TEAMS = RF.charDB.maxTeams
    end

    -- Apply high contrast mode if enabled
    if RF.ApplyHighContrast then
        RF:ApplyHighContrast()
    end
end

-------------------------------------------------------------------------------
-- Confirm Dialog Utility
-------------------------------------------------------------------------------
function RF:ShowConfirmDialog(title, message, onConfirm)
    if not RF._confirmDialog then
        local f = RF:CreateStyledFrame("RosterForgeConfirm", UIParent, 320, 130, {
            title = title or "Confirm",
            closable = true,
            strata = "DIALOG",
        })
        f:SetPoint("CENTER")
        f:Hide()

        local body = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        body:SetFont(body:GetFont(), RF.FONT_SIZES.normal, "")
        body:SetPoint("TOPLEFT", f.titleBar, "BOTTOMLEFT", 12, -12)
        body:SetPoint("TOPRIGHT", f.titleBar, "BOTTOMRIGHT", -12, -12)
        body:SetTextColor(unpack(RF.Theme.textPrimary))
        body:SetJustifyH("CENTER")
        body:SetWordWrap(true)
        f.body = body

        local confirmBtn = RF:CreateButton(f, "Confirm", 90, 26, "danger")
        confirmBtn:SetPoint("BOTTOMRIGHT", f, "BOTTOM", -4, 12)
        f.confirmBtn = confirmBtn

        local cancelBtn = RF:CreateButton(f, "Cancel", 90, 26)
        cancelBtn:SetPoint("BOTTOMLEFT", f, "BOTTOM", 4, 12)
        cancelBtn:SetScript("OnClick", function() f:Hide() end)

        RF._confirmDialog = f
    end

    local f = RF._confirmDialog
    f.titleText:SetText(title or "Confirm")
    f.body:SetText(message or "Are you sure?")
    f.confirmBtn:SetScript("OnClick", function()
        f:Hide()
        if onConfirm then onConfirm() end
    end)
    f:Show()
end

-------------------------------------------------------------------------------
-- Guild Key
-------------------------------------------------------------------------------
function RF:GetGuildKey()
    local guildName, _, _, realm = GetGuildInfo("player")
    if not guildName then
        -- GetGuildInfo returns nil for several seconds after login;
        -- IsInGuild() may also return false during that window.
        -- Fall back to the key cached during the previous session so
        -- guild data (chat log, etc.) is available immediately.
        if RF.charDB and RF.charDB.lastGuildKey then
            return RF.charDB.lastGuildKey
        end
        -- No cached key either -- nothing we can do yet
        return nil
    end
    -- GetGuildInfo can return nil or "" for realm depending on timing;
    -- normalize to always use GetRealmName() when the value is missing
    if not realm or realm == "" then
        realm = GetRealmName()
    end
    local key = guildName .. "-" .. realm
    -- Cache for next login so chat/data loads before GetGuildInfo is ready
    if RF.charDB then
        RF.charDB.lastGuildKey = key
    end
    return key
end

function RF:GetGuildDB()
    local key = self:GetGuildKey()
    if not key then return nil end
    -- Migrate orphaned data from old buggy keys (empty realm suffix)
    if not RF._guildKeyMigrated and RF.db.global.guilds then
        RF._guildKeyMigrated = true
        local guildName = key:match("^(.+)%-")
        if guildName then
            local badKey = guildName .. "-"
            local orphan = RF.db.global.guilds[badKey]
            if orphan and badKey ~= key then
                -- Merge orphan into correct key if it exists, otherwise adopt it
                if not RF.db.global.guilds[key] then
                    RF.db.global.guilds[key] = orphan
                else
                    -- Merge favorites
                    if orphan.favorites then
                        if not RF.db.global.guilds[key].favorites then
                            RF.db.global.guilds[key].favorites = {}
                        end
                        for n, v in pairs(orphan.favorites) do
                            RF.db.global.guilds[key].favorites[n] = v
                        end
                    end
                    -- Merge chatLog
                    if orphan.chatLog and #orphan.chatLog > 0 then
                        if not RF.db.global.guilds[key].chatLog then
                            RF.db.global.guilds[key].chatLog = {}
                        end
                        for _, entry in ipairs(orphan.chatLog) do
                            tinsert(RF.db.global.guilds[key].chatLog, entry)
                        end
                    end
                    -- Merge applicantHistory
                    if orphan.applicantHistory and #orphan.applicantHistory > 0 then
                        if not RF.db.global.guilds[key].applicantHistory then
                            RF.db.global.guilds[key].applicantHistory = {}
                        end
                        for _, entry in ipairs(orphan.applicantHistory) do
                            tinsert(RF.db.global.guilds[key].applicantHistory, entry)
                        end
                    end
                end
                RF.db.global.guilds[badKey] = nil
            end
        end
    end
    if not RF.db.global.guilds[key] then
        RF.db.global.guilds[key] = {
            tracking = {
                members = {},   -- per-member tracking data
                banList = {},
                log = {},
            },
            alts = {
                links = {},     -- altName -> mainName
                mains = {},     -- mainName -> { alt1, alt2, ... }
                timestamps = {}, -- altName -> unixTimestamp (last-modified)
            },
            notes = {},         -- extended notes: name -> text
            chatLog = {},       -- persisted guild chat history
            memberCache = {},   -- last-known data per member (m+, ilvl, zone, level, etc.)
            newsLog = {},       -- persisted guild news entries
        }
    end
    -- Ensure core structures exist (handles purged or legacy databases)
    local gdb = RF.db.global.guilds[key]
    if not gdb.tracking then gdb.tracking = { members = {}, banList = {}, log = {} } end
    if not gdb.tracking.members then gdb.tracking.members = {} end
    if not gdb.tracking.banList then gdb.tracking.banList = {} end
    if not gdb.tracking.log then gdb.tracking.log = {} end
    if not gdb.alts then gdb.alts = { links = {}, mains = {}, timestamps = {} } end
    if not gdb.alts.links then gdb.alts.links = {} end
    if not gdb.alts.mains then gdb.alts.mains = {} end
    if not gdb.alts.timestamps then gdb.alts.timestamps = {} end
    if not gdb.notes then gdb.notes = {} end
    if not gdb.memberCache then gdb.memberCache = {} end
    if not gdb.newsLog then gdb.newsLog = {} end
    if not gdb.chatLog then gdb.chatLog = {} end
    if not gdb.eventLog then gdb.eventLog = {} end
    if not gdb.favorites then gdb.favorites = {} end
    if not gdb.tags then gdb.tags = {} end
    if not gdb.lastSessionRoster then gdb.lastSessionRoster = {} end
    if not gdb.applicantHistory then gdb.applicantHistory = {} end
    if not gdb.analytics then gdb.analytics = { snapshots = {}, lastSnapshotTime = 0 } end
    if not gdb.dismissedActionItems then gdb.dismissedActionItems = {} end
    if not gdb.noteTimestamps then gdb.noteTimestamps = {} end
    if not gdb.teams then gdb.teams = {} end
    if not gdb.teamTimestamps then gdb.teamTimestamps = {} end
    -- MOTD tracking (for stale MOTD action item)
    -- lastMotd and lastMotdTime are set dynamically on GUILD_MOTD event
    return gdb
end

-------------------------------------------------------------------------------
-- Favorites
-------------------------------------------------------------------------------
function RF:IsFavorite(name)
    local gdb = self:GetGuildDB()
    return gdb and gdb.favorites[name] or false
end

function RF:SetFavorite(name, state)
    local gdb = self:GetGuildDB()
    if not gdb then return end
    gdb.favorites[name] = state or nil
end

-------------------------------------------------------------------------------
-- URL Helpers
-------------------------------------------------------------------------------
local URL_PATTERN = "(https?://[%w%-%.%_%~%:%/%?%#%[%]%@%!%$%&%'%(%)%*%+%,%;%%=]+)"

function RF:WrapURLs(text)
    if not text then return text end
    return text:gsub(URL_PATTERN, "|cFF5BA8FF|Hurl:%1|h[%1]|h|r")
end

function RF:ExtractURL(text)
    if not text then return nil end
    return text:match(URL_PATTERN)
end

-- Color URLs blue for FontStrings (no hyperlink markup)
function RF:ColorURLs(text)
    if not text then return text end
    return text:gsub(URL_PATTERN, "|cFF5BA8FF%1|r")
end

-------------------------------------------------------------------------------
-- Toggle Main Frame
-------------------------------------------------------------------------------
function RF:Toggle()
    if RF.mainFrame then
        if RF.mainFrame:IsShown() then
            RF.mainFrame:Hide()
        else
            RF.mainFrame:Show()
            -- Request fresh roster data
            if IsInGuild() then
                C_GuildInfo.GuildRoster()
            end
        end
    end
end

function RF:Show()
    if RF.mainFrame then
        RF.mainFrame:Show()
        if IsInGuild() then
            C_GuildInfo.GuildRoster()
        end
    end
end

function RF:Hide()
    if RF.mainFrame then
        RF.mainFrame:Hide()
    end
end

-------------------------------------------------------------------------------
-- Slash Commands
-------------------------------------------------------------------------------
local function HandleSlashCommand(input)
    local raw = (input or ""):trim()
    local msg = raw:lower()

    if msg == "" or msg == "toggle" then
        RF:Toggle()
    elseif msg == "show" or msg == "open" then
        RF:Show()
    elseif msg == "hide" or msg == "close" then
        RF:Hide()
    elseif msg == "settings" or msg == "config" then
        RF:Toggle()
        if RF.mainFrame and RF.mainFrame:IsShown() then
            RF:FireCallbacks("SWITCH_TAB", "Settings")
        end
    elseif msg == "compact" then
        RF:Show()
        if RF.ToggleCompactMode then
            RF:ToggleCompactMode()
        end
    elseif msg:sub(1, 5) == "note " then
        local rest = raw:sub(6):trim()
        local playerName, noteText = rest:match("^(%S+)%s+(.+)$")
        if playerName and noteText then
            RF:SetExtendedNote(playerName, noteText)
            RF:Print("RosterNote set for " .. playerName)
        elseif rest ~= "" then
            local existing = RF:GetExtendedNote(rest)
            if existing then
                RF:Print("RosterNote for " .. rest .. ": " .. existing)
            else
                RF:Print("No RosterNote for " .. rest)
            end
        else
            RF:Print("Usage: /rf note PlayerName your note text here")
        end
    elseif msg == "testteams" then
        if RF.charDB and RF.charDB.debugMode then
            RF:PopulateTestTeams()
        else
            RF:Print("Debug mode required. Enable in Settings.")
        end
    elseif msg == "debug" then
        if RF.charDB and RF.charDB.debugMode then
            RF:PrintDebug()
        else
            RF:Print("Debug mode required. Enable in Settings.")
        end
    elseif msg == "help" then
        RF:Print("Commands: /rf [toggle|show|hide|settings|compact|note|help]")
        RF:Print("  /rf note PlayerName text - Set a RosterNote")
        RF:Print("  /rf note PlayerName - View existing RosterNote")
    else
        RF:Print("Unknown command: " .. msg .. ". Type /rf help")
    end
end

SLASH_ROSTERFORGE1 = "/rf"
SLASH_ROSTERFORGE2 = "/rosterforge"
SlashCmdList["ROSTERFORGE"] = HandleSlashCommand

-- Register custom slash command from saved settings (applied on load/reload)
RF:RegisterCallback("PLAYER_ENTERING_WORLD", function()
    if RF.charDB and RF.charDB.customSlashCommand and RF.charDB.customSlashCommand ~= "" then
        local cmd = RF.charDB.customSlashCommand:lower():gsub("^/", "")
        if cmd ~= "rf" and cmd ~= "rosterforge" and cmd ~= "" then
            SLASH_ROSTERFORGE3 = "/" .. cmd
            -- Re-register to pick up the new alias
            SlashCmdList["ROSTERFORGE"] = HandleSlashCommand
        end
    end
end)

-------------------------------------------------------------------------------
-- Chat Output
-------------------------------------------------------------------------------
function RF:Print(msg)
    local prefix = "|cFF3B82F6RosterForge|r: "
    DEFAULT_CHAT_FRAME:AddMessage(prefix .. tostring(msg))
end

function RF:Debug(msg)
    if RF.charDB and RF.charDB.debugMode then
        RF:Print("|cFF888888[debug]|r " .. tostring(msg))
    end
end

function RF:PrintDebug()
    if not RF.charDB or not RF.charDB.debugMode then
        -- Always allow /rf debug to print basic stats
    end
    UpdateAddOnMemoryUsage()
    local mem = GetAddOnMemoryUsage(addonName)
    local memStr
    if mem > 1024 then
        memStr = format("%.1f MB", mem / 1024)
    else
        memStr = format("%.0f KB", mem)
    end
    RF:Print("Memory: " .. memStr)
    if RF.rosterData then
        RF:Print("Cached members: " .. #RF.rosterData)
    end
    local guildDB = RF:GetGuildDB()
    if guildDB then
        local tracked = 0
        for _ in pairs(guildDB.tracking.members) do tracked = tracked + 1 end
        RF:Print("Tracked members: " .. tracked)
        local alts = 0
        for _ in pairs(guildDB.alts.links) do alts = alts + 1 end
        RF:Print("Alt links: " .. alts)
        local cached = 0
        if guildDB.memberCache then
            for _ in pairs(guildDB.memberCache) do cached = cached + 1 end
        end
        RF:Print("Cached member data: " .. cached)
        local chatMsgs = guildDB.chatLog and #guildDB.chatLog or 0
        RF:Print("Persisted chat messages: " .. chatMsgs)
        local notes = 0
        for _ in pairs(guildDB.notes) do notes = notes + 1 end
        RF:Print("RosterNotes: " .. notes)
    end
    RF:Print("Guild key: " .. tostring(RF:GetGuildKey()))
end

-------------------------------------------------------------------------------
-- Utility: Time Formatting
-------------------------------------------------------------------------------
function RF:TimeToSeconds(years, months, days, hours)
    years  = years or 0
    months = months or 0
    days   = days or 0
    hours  = hours or 0
    return ((years * 365 + months * 30 + days) * 24 + hours) * 3600
end

function RF:FormatTimeOffline(years, months, days, hours)
    if years and years > 0 then
        return format("%dy %dm", years, months)
    elseif months and months > 0 then
        return format("%dm %dd", months, days)
    elseif days and days > 0 then
        return format("%dd %dh", days, hours)
    elseif hours and hours > 0 then
        return format("%dh", hours)
    else
        return "Online"
    end
end

function RF:FormatTimestamp(timestamp)
    if not timestamp then return "Unknown" end
    return date("%Y-%m-%d", timestamp)
end

function RF:FormatTimestampChat(timestamp)
    if not timestamp then return "" end
    local fmt = RF.charDB and RF.charDB.chatTimestampFormat or "12h"
    if fmt == "12h" then
        return date("%I:%M %p", timestamp)
    elseif fmt == "relative" then
        local diff = time() - timestamp
        if diff < 60 then
            return "now"
        elseif diff < 3600 then
            return format("%dm ago", math.floor(diff / 60))
        elseif diff < 86400 then
            return format("%dh ago", math.floor(diff / 3600))
        else
            return format("%dd ago", math.floor(diff / 86400))
        end
    else
        return date("%H:%M", timestamp)
    end
end

-------------------------------------------------------------------------------
-- Utility: Class Color
-------------------------------------------------------------------------------
function RF:GetClassColor(classFile)
    if RAID_CLASS_COLORS and RAID_CLASS_COLORS[classFile] then
        return RAID_CLASS_COLORS[classFile]
    end
    return RF.CLASS_COLORS[classFile] or { r = 0.7, g = 0.7, b = 0.7 }
end

function RF:GetClassColoredName(name, classFile)
    local c = self:GetClassColor(classFile)
    return format("|cFF%02x%02x%02x%s|r", c.r * 255, c.g * 255, c.b * 255, name)
end

-------------------------------------------------------------------------------
-- Event Handler
-------------------------------------------------------------------------------
local function OnEvent(self, event, ...)
    if event == "ADDON_LOADED" and ... == addonName then
        RF:InitDB()
        RF:Print("v" .. RF.VERSION .. " loaded. Type /rf to open.")

        -- Request guild roster if in guild
        if IsInGuild() then
            C_GuildInfo.GuildRoster()
        end

        -- Fire init callbacks (UI modules hook here)
        RF:FireCallbacks("ADDON_LOADED")

        self:UnregisterEvent("ADDON_LOADED")

    elseif event == "PLAYER_REGEN_ENABLED" then
        RF:FireCallbacks("PLAYER_REGEN_ENABLED")

    elseif event == "PLAYER_ENTERING_WORLD" then
        RF:FireCallbacks("PLAYER_ENTERING_WORLD", ...)
        if IsInGuild() then
            C_Timer.After(2, function()
                C_GuildInfo.GuildRoster()
                if not RF._challengeRequested and RequestGuildChallengeInfo then
                    RF._challengeRequested = true
                    RequestGuildChallengeInfo()
                end
            end)
        end
        -- Fallback: request challenge data after 5s even if IsInGuild()
        -- returned false at login (guild system loads asynchronously)
        C_Timer.After(5, function()
            if not RF._challengeRequested then
                if not RequestGuildChallengeInfo then
                    pcall(function()
                        if C_AddOns and C_AddOns.LoadAddOn then
                            C_AddOns.LoadAddOn("Blizzard_GuildUI")
                        elseif LoadAddOn then
                            LoadAddOn("Blizzard_GuildUI")
                        end
                    end)
                end
                if RequestGuildChallengeInfo then
                    RF._challengeRequested = true
                    RequestGuildChallengeInfo()
                end
            end
        end)

    elseif event == "GUILD_ROSTER_UPDATE" then
        local canRequestRoster = ...
        if canRequestRoster then
            C_GuildInfo.GuildRoster()
        end
        -- Request challenge data once the guild system is confirmed ready
        -- (IsInGuild() may return false on login, so PLAYER_ENTERING_WORLD
        -- can miss this; GUILD_ROSTER_UPDATE only fires when guild is loaded)
        if not RF._challengeRequested then
            -- Ensure LoD addon providing challenge API is loaded
            if not RequestGuildChallengeInfo then
                pcall(function()
                    if C_AddOns and C_AddOns.LoadAddOn then
                        C_AddOns.LoadAddOn("Blizzard_GuildUI")
                    elseif LoadAddOn then
                        LoadAddOn("Blizzard_GuildUI")
                    end
                end)
            end
            if RequestGuildChallengeInfo then
                RF._challengeRequested = true
                RequestGuildChallengeInfo()
            end
        end
        RF:FireCallbacks("GUILD_ROSTER_UPDATE")

    elseif event == "GUILD_MOTD" then
        -- Track when MOTD last changed
        local gdb = RF:GetGuildDB()
        if gdb then
            local newMotd = ...
            if newMotd and newMotd ~= "" then
                if not gdb.lastMotd or gdb.lastMotd ~= newMotd then
                    gdb.lastMotdTime = time()
                    gdb.lastMotd = newMotd
                end
            end
        end
        RF:FireCallbacks("GUILD_MOTD", ...)

    elseif event == "GUILD_NEWS_UPDATE" then
        RF:FireCallbacks("GUILD_NEWS_UPDATE")

    elseif event == "GUILD_RANKS_UPDATE" then
        RF:FireCallbacks("GUILD_RANKS_UPDATE")

    elseif event == "CHAT_MSG_GUILD" or event == "CHAT_MSG_OFFICER" then
        -- Ambiguate must be called here in the untainted event handler;
        -- passing raw secret sender values through callbacks causes taint.
        local message, sender = ...
        local safeSender = sender and Ambiguate(sender, "guild") or "Unknown"
        RF:FireCallbacks(event, message, safeSender)

    elseif event == "GUILD_EVENT_LOG_UPDATE" then
        RF:FireCallbacks("GUILD_EVENT_LOG_UPDATE")

    elseif event == "GUILD_CHALLENGE_UPDATED" then
        RF:FireCallbacks("GUILD_CHALLENGE_UPDATED")

    elseif event == "GUILD_CHALLENGE_COMPLETED" then
        RF:FireCallbacks("GUILD_CHALLENGE_COMPLETED", ...)

    elseif event == "GUILD_REWARDS_LIST" then
        RF:FireCallbacks("GUILD_REWARDS_LIST")

    elseif event == "CLUB_FINDER_RECRUIT_LIST_CHANGED" then
        RF:FireCallbacks("CLUB_FINDER_RECRUIT_LIST_CHANGED")

    elseif event == "CLUB_FINDER_RECRUITS_UPDATED" then
        RF:FireCallbacks("CLUB_FINDER_RECRUITS_UPDATED")

    elseif event == "CLUB_FINDER_APPLICATIONS_UPDATED" then
        RF:FireCallbacks("CLUB_FINDER_APPLICATIONS_UPDATED")

    elseif event == "CHAT_MSG_SYSTEM" then
        RF:FireCallbacks("CHAT_MSG_SYSTEM", ...)

    elseif event == "CLUB_MEMBER_ADDED" or event == "CLUB_MEMBER_REMOVED" or event == "CLUB_MEMBER_UPDATED" then
        RF:FireCallbacks(event, ...)

    elseif event == "PLAYER_GUILD_UPDATE" then
        -- If the player left their guild, clear the cached key so we don't
        -- keep returning a stale guild DB for a guild they no longer belong to.
        if not IsInGuild() and RF.charDB then
            RF.charDB.lastGuildKey = nil
        end
        RF:FireCallbacks("PLAYER_GUILD_UPDATE", ...)
    end
end

eventFrame:SetScript("OnEvent", OnEvent)
eventFrame:RegisterEvent("ADDON_LOADED")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")

-- Register guild events
for _, event in ipairs(RF.GUILD_EVENTS) do
    eventFrame:RegisterEvent(event)
end

-------------------------------------------------------------------------------
-- Keybinding Support
-- Create a secure button so the binding system can target it
-------------------------------------------------------------------------------
BINDING_HEADER_ROSTERFORGE = "RosterForge"
BINDING_NAME_ROSTERFORGE_TOGGLE = "Toggle RosterForge"

local toggleBtn = CreateFrame("Button", "RosterForgeToggleBinding", UIParent, "SecureActionButtonTemplate")
toggleBtn:SetSize(1, 1)
toggleBtn:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -100, 100)
toggleBtn:RegisterForClicks("AnyUp")
toggleBtn:SetScript("OnClick", function() RF:Toggle() end)
RF.toggleBtn = toggleBtn
