-- Rack & Ruin: Network.lua
-- Shared multiplayer session layer. Uses the player's party as the "table":
-- the party leader is the host/dealer and owns authoritative game state.
-- Other members send action requests to the host; the host broadcasts state updates.
--
-- Solo (no party) play still works unchanged - IsHost() is always true and
-- Send() becomes a no-op, so existing single-player game logic doesn't need
-- to know or care whether it's networked.

RackAndRuin = RackAndRuin or {}
RackAndRuin.Net = RackAndRuin.Net or {}

local Net = RackAndRuin.Net
local ADDON_PREFIX = "RACKRUIN1" -- must be <= 16 chars, registered below

-- gameId -> { msgType -> { callback, callback, ... } }
local handlers = {}
-- callbacks fired whenever host status or roster changes: fn(isHost, players)
local statusListeners = {}

local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
eventFrame:RegisterEvent("PARTY_LEADER_CHANGED")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("CHAT_MSG_ADDON")

C_ChatInfo.RegisterAddonMessagePrefix(ADDON_PREFIX)

-- Returns true if currently grouped (party or raid)
function Net.IsGrouped()
    return IsInGroup()
end

-- Returns true if this client is the authoritative host for the table.
-- Solo play: always host. Grouped: only the party/raid leader is host.
function Net.IsHost()
    if not IsInGroup() then
        return true
    end
    return UnitIsGroupLeader("player")
end

-- Returns an ordered list of player full names at the table (self included).
-- Solo play returns just the local player.
function Net.GetPlayers()
    local players = { UnitName("player") }
    if IsInGroup() then
        local numMembers = GetNumGroupMembers()
        local unitPrefix = IsInRaid() and "raid" or "party"
        for i = 1, numMembers do
            local unit = unitPrefix .. i
            if UnitExists(unit) and not UnitIsUnit(unit, "player") then
                local name = UnitName(unit)
                if name then table.insert(players, name) end
            end
        end
    end
    return players
end

-- Returns the host's player name (party leader, or self if solo/host).
function Net.GetHostName()
    if not IsInGroup() then
        return UnitName("player")
    end
    if UnitIsGroupLeader("player") then
        return UnitName("player")
    end
    local unitPrefix = IsInRaid() and "raid" or "party"
    local numMembers = GetNumGroupMembers()
    for i = 1, numMembers do
        local unit = unitPrefix .. i
        if UnitExists(unit) and UnitIsGroupLeader(unit) then
            return UnitName(unit)
        end
    end
    return UnitName("player")
end

local function GetChannel()
    if IsInRaid() then return "RAID" end
    if IsInGroup() then return "PARTY" end
    return nil
end

-- Serializes a table of simple key/value pairs (strings/numbers only) into
-- a compact string. Not general-purpose - just enough for card/game state.
function Net.Encode(tbl)
    local parts = {}
    for k, v in pairs(tbl) do
        table.insert(parts, tostring(k) .. "=" .. tostring(v))
    end
    return table.concat(parts, ";")
end

function Net.Decode(str)
    local tbl = {}
    for pair in string.gmatch(str, "[^;]+") do
        local k, v = string.match(pair, "([^=]+)=(.*)")
        if k then tbl[k] = v end
    end
    return tbl
end

-- Encodes a hand (array of {rank=, suit=}) as "A,S|10,D|K,H"
function Net.EncodeHand(hand)
    local parts = {}
    for _, card in ipairs(hand) do
        table.insert(parts, card.rank .. "," .. card.suit)
    end
    return table.concat(parts, "|")
end

function Net.DecodeHand(str)
    local hand = {}
    if not str or str == "" then return hand end
    for entry in string.gmatch(str, "[^|]+") do
        local rank, suit = string.match(entry, "([^,]+),([^,]+)")
        if rank and suit then
            table.insert(hand, {
                rank = rank,
                suit = suit,
                texture = RackAndRuin.GetCardTexture(rank, suit),
            })
        end
    end
    return hand
end

-- Registers a handler for a given gameId + msgType.
-- gameId: e.g. "BJ" or "POKER" - namespaces messages per game.
-- msgType: e.g. "HIT", "STATE", "DEAL"
-- callback(senderName, payloadString)
function Net.RegisterHandler(gameId, msgType, callback)
    handlers[gameId] = handlers[gameId] or {}
    handlers[gameId][msgType] = handlers[gameId][msgType] or {}
    table.insert(handlers[gameId][msgType], callback)
end

-- Sends a message to the table. No-op (returns false) if not grouped.
-- gameId: "BJ" / "POKER", msgType: "HIT" / "STATE" / etc, payload: string
function Net.Send(gameId, msgType, payload)
    local channel = GetChannel()
    if not channel then return false end
    local wireMsg = gameId .. ":" .. msgType .. ":" .. (payload or "")
    C_ChatInfo.SendAddonMessage(ADDON_PREFIX, wireMsg, channel)
    return true
end

-- Sends directly to a specific player (used by host replying to one client,
-- or a client sending a request only the host needs to see).
function Net.SendWhisper(gameId, msgType, payload, targetName)
    local wireMsg = gameId .. ":" .. msgType .. ":" .. (payload or "")
    C_ChatInfo.SendAddonMessage(ADDON_PREFIX, wireMsg, "WHISPER", targetName)
end

function Net.RegisterStatusListener(callback)
    table.insert(statusListeners, callback)
end

local function FireStatusListeners()
    local isHost = Net.IsHost()
    local players = Net.GetPlayers()
    for _, cb in ipairs(statusListeners) do
        cb(isHost, players)
    end
end

eventFrame:SetScript("OnEvent", function(self, event, prefix, message, channel, sender)
    if event == "GROUP_ROSTER_UPDATE" or event == "PARTY_LEADER_CHANGED" or event == "PLAYER_ENTERING_WORLD" then
        FireStatusListeners()
        return
    end

    if event == "CHAT_MSG_ADDON" then
        if prefix ~= ADDON_PREFIX then return end
        -- sender comes through as "Name-Realm" sometimes; normalize to short name
        local shortSender = strsplit("-", sender)

        local gameId, msgType, payload = string.match(message, "^([^:]+):([^:]+):(.*)$")
        if not gameId then return end

        if handlers[gameId] and handlers[gameId][msgType] then
            for _, cb in ipairs(handlers[gameId][msgType]) do
                cb(shortSender, payload)
            end
        end
    end
end)
