local ADDON_NAME = "GroupForge"
local GF = {}
_G[ADDON_NAME] = GF

GF.version = "1.0.0"
GF.PREFIX = "GroupForge"
GF.CHANNEL_NAME = "GroupForgeSync"

local defaults = {
    enabled = true,
    minimapAngle = 45,
    minimap = { hide = false },
}

GF.activeGroupId = nil
GF.myCharacter = nil
GF.leaderCharacter = nil
GF.roster = {} -- { ["Name-Realm"] = true/false }
GF.rosterList = {}
GF.isLeader = false

-- ── Helpers ───────────────────────────────────────────────────────────────────
local function NormalizeName(str)
    return str and str:lower():gsub("[%s%-']", "") or ""
end

local function CanonicalCharacterName(str)
    if not str then return nil end
    str = str:gsub("^%s+", ""):gsub("%s+$", "")
    local name, realm = strsplit("-", str)
    if realm and realm ~= "" then
        return name .. "-" .. realm:gsub("%s+", "")
    end
    return str
end

-- ── Base64 Decode ─────────────────────────────────────────────────────────────
local b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
local function b64decode(data)
    data = string.gsub(data, '[^'..b64chars..'=]', '')
    return (data:gsub('.', function(x)
        if (x == '=') then return '' end
        local r,f='',(b64chars:find(x)-1)
        for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
        return r;
    end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
        if (#x ~= 8) then return '' end
        local c=0
        for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
        return string.char(c)
    end))
end

-- ── Event frame ──────────────────────────────────────────────────────────────

local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("ADDON_LOADED")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("PARTY_INVITE_REQUEST")
eventFrame:RegisterEvent("CHAT_MSG_ADDON")

eventFrame:SetScript("OnEvent", function(_, event, ...)
    if event == "ADDON_LOADED" then
        if (...) == ADDON_NAME then GF:Init() end
    elseif event == "PLAYER_ENTERING_WORLD" then
        GF:JoinSyncChannel()
    elseif event == "PARTY_INVITE_REQUEST" then
        GF:OnInviteRequest(...)
    elseif event == "CHAT_MSG_ADDON" then
        GF:OnAddonMessage(...)
    end
end)

-- ── Init ─────────────────────────────────────────────────────────────────────

function GF:Init()
    if not GroupForgeDB then GroupForgeDB = CopyTable(defaults) end

    C_ChatInfo.RegisterAddonMessagePrefix(self.PREFIX)
    self:RegisterSlash()
    
    if self.UI then self.UI:Init() end
    print("|cffFAC800GroupForge|r v" .. self.version .. " loaded. Type |cff808080/gforge|r to open.")
end

function GF:JoinSyncChannel()
    local channelId = GetChannelName(self.CHANNEL_NAME)
    if channelId == 0 then
        JoinTemporaryChannel(self.CHANNEL_NAME)
    end
    -- Hide from all chat frames so it doesn't clutter the General tab
    for i = 1, NUM_CHAT_WINDOWS do
        local frame = _G["ChatFrame" .. i]
        if frame then ChatFrame_RemoveChannel(frame, self.CHANNEL_NAME) end
    end
end

-- ── Payload Handling ──────────────────────────────────────────────────────────

function GF:ParsePayload(rawPayload)
    -- Format: b64.signature
    local parts = { strsplit(".", rawPayload) }
    if #parts ~= 2 then return false, "Invalid payload format." end
    
    local decoded = b64decode(parts[1])
    if not decoded or decoded == "" then return false, "Failed to decode payload." end
    
    -- Decoded format: GroupForge.groupId.myName.leaderName.roster.contentType.name.modifier.expiry
    local dParts = { strsplit(".", decoded) }
    if #dParts < 9 then return false, "Malformed payload data." end

    local version       = dParts[1]
    local groupId       = dParts[2]
    local myName        = dParts[3]
    local leaderName    = dParts[4]
    local rosterStr     = dParts[5]
    local contentType   = dParts[6]
    local contentName   = dParts[7]
    local contentModifier = dParts[8]
    local expiry        = tonumber(dParts[9])
    
    if version ~= self.PREFIX and version ~= "GF3" then return false, "Unsupported payload version." end
    if expiry and time() > expiry then return false, "Payload has expired." end

    myName = CanonicalCharacterName(myName)
    leaderName = CanonicalCharacterName(leaderName)
    
    -- Verify myName matches actual player
    local pName, pRealm = UnitFullName("player")
    -- In some edge cases Realm can be nil with spaces, ensure safe formatting
    pRealm = pRealm and pRealm:gsub("%s+", "") or GetRealmName():gsub("%s+", "")
    local expectedMe = pName .. "-" .. pRealm
    
    if NormalizeName(myName) ~= NormalizeName(expectedMe) then
        return false, "Payload is for a different character ("..myName..")."
    end

    self.activeGroupId    = groupId
    self.myCharacter      = myName
    self.leaderCharacter  = leaderName
    self.isLeader         = (myName == leaderName)
    self.contentType      = contentType
    self.contentName      = contentName
    self.contentModifier  = contentModifier
    
    self.roster = {}
    self.rosterList = {}
    self.rosterInfo = {} -- { ["Name-Realm"] = { role = "dps", classId = "1" } }
    
    -- Parse roster
    local members = { strsplit(",", rosterStr) }
    local reservedCount = 0
    for _, memberData in ipairs(members) do
        if memberData and memberData ~= "" then
            local mName, mRole, mClassId = strsplit(":", memberData)
            if mName and mName ~= "" then
                if mName == "Reserved" then
                    -- Friend reservation: auto-ready, the leader will invite directly
                    reservedCount = reservedCount + 1
                    local key = "Reserved_" .. reservedCount
                    table.insert(self.rosterList, key)
                    self.roster[key] = true
                    self.rosterInfo[key] = { role = mRole or "dps", classId = 0, reserved = true }
                else
                    mName = CanonicalCharacterName(mName)
                    table.insert(self.rosterList, mName)
                    self.roster[mName] = (mName == myName)
                    self.rosterInfo[mName] = { role = mRole or "dps", classId = tonumber(mClassId) or 0 }
                end
            end
        end
    end
    
    -- Broadcast ready
    self:BroadcastReady()

    if self.UI then self.UI:RefreshRoster() end

    return true, "Ready."
end

function GF:CancelGroup()
    self.activeGroupId    = nil
    self.myCharacter      = nil
    self.leaderCharacter  = nil
    self.roster           = {}
    self.rosterList       = {}
    self.rosterInfo       = {}
    self.isLeader         = false
    self.contentType      = nil
    self.contentName      = nil
    self.contentModifier  = nil
    if self.UI then self.UI:RefreshRoster() end
end

function GF:BroadcastReady()
    if not self.activeGroupId then return end
    local msg = "READY|" .. self.activeGroupId .. "|" .. self.myCharacter

    if self.leaderCharacter and self.leaderCharacter ~= self.myCharacter then
        C_ChatInfo.SendAddonMessage(self.PREFIX, msg, "WHISPER", self.leaderCharacter)
        print("|cffFAC800GroupForge|r Ready sent to " .. self.leaderCharacter)
    end

    local channelId = GetChannelName(self.CHANNEL_NAME)
    if channelId > 0 then
        C_ChatInfo.SendAddonMessage(self.PREFIX, msg, "CHANNEL", channelId)
    end
end

function GF:SendReadyTo(characterName)
    if not self.activeGroupId or not self.myCharacter or not characterName then return end
    local msg = "READY|" .. self.activeGroupId .. "|" .. self.myCharacter
    C_ChatInfo.SendAddonMessage(self.PREFIX, msg, "WHISPER", characterName)
end

function GF:OnAddonMessage(prefix, text, channel, sender, target, zoneChannelID, localID, name, instanceID)
    if prefix ~= self.PREFIX then return end

    local cmd, groupId, charName
    if text:find("|", 1, true) then
        cmd, groupId, charName = strsplit("|", text)
    else
        cmd, groupId, charName = strsplit(" ", text)
    end

    charName = CanonicalCharacterName(charName)
    if cmd == "READY" and groupId == self.activeGroupId then
        if self.roster[charName] ~= nil then
            local wasReady = self.roster[charName]
            self.roster[charName] = true
            if self.UI then self.UI:RefreshRoster() end

            if charName ~= self.myCharacter then
                if self.isLeader then
                    self:SendReadyTo(charName)
                    if not wasReady then
                        print("|cffFAC800GroupForge|r Ready from " .. charName .. ". Inviting...")
                        C_PartyInfo.InviteUnit(charName)
                    end
                end
            end
        end
    end
end

-- ── Group Management ──────────────────────────────────────────────────────────

function GF:OnInviteRequest(inviter, _, _, _, _, _, _, isXRealm)
    -- In PARTY_INVITE_REQUEST, inviter might not have realm if same realm
    if not self.activeGroupId or not self.leaderCharacter then return end
    
    local invName, invRealm = strsplit("-", inviter)
    if not invRealm then
        invRealm = GetRealmName():gsub("%s+", "")
        inviter = invName .. "-" .. invRealm
    end
    
    -- Cross-check leader
    if NormalizeName(inviter) == NormalizeName(self.leaderCharacter) then
        AcceptGroup()
        StaticPopup_Hide("PARTY_INVITE")
        print("|cffFAC800GroupForge|r Auto-accepted invite from " .. inviter)
    end
end

function GF:StartGroup()
    if not self.isLeader then return end
    for name, _ in pairs(self.roster) do
        local info = self.rosterInfo and self.rosterInfo[name]
        if name ~= self.myCharacter and not (info and info.reserved) then
            C_PartyInfo.InviteUnit(name)
        end
    end
end

-- ── Slash commands ────────────────────────────────────────────────────────────

function GF:RegisterSlash()
    SLASH_GROUPFORGE1 = "/gforge"
    SLASH_GROUPFORGE2 = "/groupforge"
    SlashCmdList["GROUPFORGE"] = function(msg)
        if self.UI then self.UI:Toggle() end
    end
end
