local ADDON_NAME = ...
local PREFIX = "GMS1"
local VERSION = "V5"

GuildeMetiersSyncDB = GuildeMetiersSyncDB or {}

local GMS = CreateFrame("Frame", "GuildeMetiersSyncFrameRoot")
_G.GuildeMetiersSync = GMS

GMS.data = {}
GMS.filtered = {}
GMS.recipeFiltered = {}
GMS.guildKey = nil
GMS.sortKey = "name"
GMS.sortAsc = true
GMS.lastBroadcast = 0
GMS.lastRequest = 0
GMS.recipeInbox = {}
GMS.lastTradeScan = 0
GMS.lastTradeSignatureByProf = {}

GMS.selectedPlayer = nil
GMS.selectedProfession = nil
GMS.selectedRecipeIndex = nil

local CUSTOM_CENTER_ART = "Interface\\AddOns\\GuildeMetiersSync\\media\\center_art"
local MAX_RECIPES_PER_PROF = 180
local MAX_ADDON_MESSAGE_LEN = 240
local TRADE_SCAN_THROTTLE = 2.5
local DEFAULT_RECIPE_ICON = "Interface\\Icons\\INV_Scroll_03"

local ONLINE_TEX = "Interface\\FriendsFrame\\StatusIcon-Online"
local OFFLINE_TEX = "Interface\\FriendsFrame\\StatusIcon-Offline"

local PRIMARY_PROFESSIONS = {
    ["Alchemy"] = true, ["Alchimie"] = true,
    ["Blacksmithing"] = true, ["Forge"] = true,
    ["Enchanting"] = true, ["Enchantement"] = true,
    ["Engineering"] = true, ["Ingénierie"] = true, ["Ingenierie"] = true,
    ["Herbalism"] = true, ["Herboristerie"] = true,
    ["Inscription"] = true, ["Calligraphie"] = true,
    ["Jewelcrafting"] = true, ["Joaillerie"] = true,
    ["Leatherworking"] = true, ["Travail du cuir"] = true,
    ["Mining"] = true, ["Minage"] = true,
    ["Skinning"] = true, ["Dépeçage"] = true, ["Depecage"] = true,
    ["Tailoring"] = true, ["Couture"] = true,
}

local PROF_COLORS = {
    ["Alchemy"] = {0.35, 0.90, 0.45}, ["Alchimie"] = {0.35, 0.90, 0.45},
    ["Blacksmithing"] = {0.72, 0.72, 0.78}, ["Forge"] = {0.72, 0.72, 0.78},
    ["Enchanting"] = {0.75, 0.45, 0.95}, ["Enchantement"] = {0.75, 0.45, 0.95},
    ["Engineering"] = {0.95, 0.65, 0.20}, ["Ingénierie"] = {0.95, 0.65, 0.20}, ["Ingenierie"] = {0.95, 0.65, 0.20},
    ["Herbalism"] = {0.40, 0.95, 0.40}, ["Herboristerie"] = {0.40, 0.95, 0.40},
    ["Inscription"] = {0.95, 0.82, 0.35}, ["Calligraphie"] = {0.95, 0.82, 0.35},
    ["Jewelcrafting"] = {0.35, 0.90, 0.95}, ["Joaillerie"] = {0.35, 0.90, 0.95},
    ["Leatherworking"] = {0.70, 0.45, 0.20}, ["Travail du cuir"] = {0.70, 0.45, 0.20},
    ["Mining"] = {0.68, 0.68, 0.68}, ["Minage"] = {0.68, 0.68, 0.68},
    ["Skinning"] = {0.85, 0.72, 0.48}, ["Dépeçage"] = {0.85, 0.72, 0.48}, ["Depecage"] = {0.85, 0.72, 0.48},
    ["Tailoring"] = {0.90, 0.55, 0.85}, ["Couture"] = {0.90, 0.55, 0.85},
}

local PROF_ICONS = {
    ["Alchemy"] = "Interface\\Icons\\Trade_Alchemy",
    ["Alchimie"] = "Interface\\Icons\\Trade_Alchemy",
    ["Blacksmithing"] = "Interface\\Icons\\Trade_BlackSmithing",
    ["Forge"] = "Interface\\Icons\\Trade_BlackSmithing",
    ["Enchanting"] = "Interface\\Icons\\Trade_Engraving",
    ["Enchantement"] = "Interface\\Icons\\Trade_Engraving",
    ["Engineering"] = "Interface\\Icons\\Trade_Engineering",
    ["Ingénierie"] = "Interface\\Icons\\Trade_Engineering",
    ["Ingenierie"] = "Interface\\Icons\\Trade_Engineering",
    ["Herbalism"] = "Interface\\Icons\\Trade_Herbalism",
    ["Herboristerie"] = "Interface\\Icons\\Trade_Herbalism",
    ["Inscription"] = "Interface\\Icons\\INV_Inscription_Tradeskill01",
    ["Calligraphie"] = "Interface\\Icons\\INV_Inscription_Tradeskill01",
    ["Jewelcrafting"] = "Interface\\Icons\\INV_Misc_Gem_01",
    ["Joaillerie"] = "Interface\\Icons\\INV_Misc_Gem_01",
    ["Leatherworking"] = "Interface\\Icons\\INV_Misc_ArmorKit_17",
    ["Travail du cuir"] = "Interface\\Icons\\INV_Misc_ArmorKit_17",
    ["Mining"] = "Interface\\Icons\\Trade_Mining",
    ["Minage"] = "Interface\\Icons\\Trade_Mining",
    ["Skinning"] = "Interface\\Icons\\INV_Misc_Pelt_Wolf_01",
    ["Dépeçage"] = "Interface\\Icons\\INV_Misc_Pelt_Wolf_01",
    ["Depecage"] = "Interface\\Icons\\INV_Misc_Pelt_Wolf_01",
    ["Tailoring"] = "Interface\\Icons\\Trade_Tailoring",
    ["Couture"] = "Interface\\Icons\\Trade_Tailoring",
}

local function msg(text)
    DEFAULT_CHAT_FRAME:AddMessage("|cff6fb1ff[GMS]|r " .. tostring(text or ""))
end

local function trimRealm(name)
    if not name then return "" end
    local dash = string.find(name, "-")
    if dash then
        return string.sub(name, 1, dash - 1)
    end
    return name
end

local function norm(s)
    if not s then return "" end
    s = string.lower(tostring(s))
    s = string.gsub(s, "[%s%-_']", "")
    return s
end

local function wipeTable(t)
    for k in pairs(t) do
        t[k] = nil
    end
end

local function split(str, sep)
    local out = {}
    if not str or str == "" then return out end
    sep = sep or ","
    for part in string.gmatch(str, "([^" .. sep .. "]+)") do
        table.insert(out, part)
    end
    return out
end

local function join(tbl, sep)
    return table.concat(tbl or {}, sep or ",")
end

local function encodeField(s)
    s = tostring(s or "")
    s = string.gsub(s, "|", "/")
    s = string.gsub(s, ";", ",")
    s = string.gsub(s, "\n", " ")
    s = string.gsub(s, "~", "-")
    s = string.gsub(s, "%^", "-")
    s = string.gsub(s, "#", "-")
    return s
end

local function decodeParts(msgText)
    local parts = {}
    for part in string.gmatch(msgText or "", "([^|]+)") do
        table.insert(parts, part)
    end
    return parts
end

local function truncateText(s, maxLen)
    s = tostring(s or "")
    if string.len(s) <= maxLen then
        return s
    end
    return string.sub(s, 1, math.max(1, maxLen - 3)) .. "..."
end

local function createBackdrop(frame)
    frame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = false,
        tileSize = 0,
        edgeSize = 14,
        insets = { left = 3, right = 3, top = 3, bottom = 3 }
    })
    frame:SetBackdropColor(0.03, 0.03, 0.04, 1)
    frame:SetBackdropBorderColor(0.38, 0.38, 0.48, 1)
end

local function createPanel(parent, left, top, right, bottom)
    local p = CreateFrame("Frame", nil, parent)
    p:SetPoint("TOPLEFT", left, top)
    p:SetPoint("BOTTOMRIGHT", right, bottom)
    createBackdrop(p)
    return p
end

local function createEditBox(parent, width, height)
    local box = CreateFrame("EditBox", nil, parent)
    box:SetAutoFocus(false)
    box:SetSize(width, height)
    box:SetFontObject(GameFontHighlightSmall)
    box:SetTextInsets(6, 6, 0, 0)
    box:SetJustifyH("LEFT")
    box:SetJustifyV("MIDDLE")
    createBackdrop(box)

    local bg = box:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetTexture("Interface\\Buttons\\WHITE8X8")
    bg:SetVertexColor(0.06, 0.06, 0.08, 1)

    box:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    box:SetScript("OnEditFocusGained", function(self) self:SetBackdropBorderColor(0.80, 0.65, 0.15, 1) end)
    box:SetScript("OnEditFocusLost", function(self) self:SetBackdropBorderColor(0.38, 0.38, 0.48, 1) end)

    return box
end

local function createStatusDot(parent, size)
    local tex = parent:CreateTexture(nil, "OVERLAY")
    tex:SetSize(size, size)
    tex:SetTexture(ONLINE_TEX)
    return tex
end

local function ensureDB()
    GuildeMetiersSyncDB = GuildeMetiersSyncDB or {}
    GuildeMetiersSyncDB.guilds = GuildeMetiersSyncDB.guilds or {}
    GuildeMetiersSyncDB.minimap = GuildeMetiersSyncDB.minimap or {
        angle = 45,
        hide = false,
    }
    GuildeMetiersSyncDB.ui = GuildeMetiersSyncDB.ui or {
        framePoint = "CENTER",
        frameX = 0,
        frameY = 0,
        selectedPlayer = nil,
        selectedProfession = nil,
        memberSearch = "",
        recipeSearch = "",
    }
end

local function getGuildKey()
    local guildName = GetGuildInfo("player")
    if not guildName then return nil end
    local realm = GetRealmName() or "UnknownRealm"
    return realm .. "::" .. guildName
end

local function getGuildDB()
    ensureDB()
    local key = getGuildKey()
    if not key then return nil end

    if not GuildeMetiersSyncDB.guilds[key] then
        GuildeMetiersSyncDB.guilds[key] = {
            members = {},
            updatedAt = time(),
        }
    end

    local db = GuildeMetiersSyncDB.guilds[key]
    db.members = db.members or {}

    return db, key
end

local function getGuildName()
    return GetGuildInfo("player") or "Sans guilde"
end

local function getClassColor(classFile)
    if RAID_CLASS_COLORS and classFile and RAID_CLASS_COLORS[classFile] then
        local c = RAID_CLASS_COLORS[classFile]
        return c.r, c.g, c.b
    end
    return 1, 1, 1
end

local function getProfessionColor(name)
    local c = PROF_COLORS[name]
    if c then return c[1], c[2], c[3] end
    return 0.85, 1.00, 0.85
end

local function getProfessionIcon(name)
    return PROF_ICONS[name] or "Interface\\Icons\\INV_Misc_QuestionMark"
end

local function getRecipeIcon(recipe)
    if not recipe then
        return DEFAULT_RECIPE_ICON
    end
    if recipe.itemString and recipe.itemString ~= "" and GetItemIcon then
        local ok, icon = pcall(GetItemIcon, recipe.itemString)
        if ok and icon then
            return icon
        end
    end
    return DEFAULT_RECIPE_ICON
end

local function formatTimeAgo(ts)
    if not ts or ts <= 0 then return "-" end
    local diff = time() - ts
    if diff < 60 then
        return diff .. " sec"
    elseif diff < 3600 then
        return math.floor(diff / 60) .. " min"
    elseif diff < 86400 then
        return math.floor(diff / 3600) .. " h"
    else
        return math.floor(diff / 86400) .. " j"
    end
end

local function saveWindowPosition()
    if not GMS.ui or not GMS.ui.frame then return end
    local point, _, _, x, y = GMS.ui.frame:GetPoint(1)
    local ui = GuildeMetiersSyncDB.ui
    ui.framePoint = point or "CENTER"
    ui.frameX = x or 0
    ui.frameY = y or 0
end

local function restoreWindowPosition(frame)
    local ui = GuildeMetiersSyncDB.ui
    frame:ClearAllPoints()
    frame:SetPoint(ui.framePoint or "CENTER", UIParent, ui.framePoint or "CENTER", ui.frameX or 0, ui.frameY or 0)
end

local function saveUIState()
    if not GuildeMetiersSyncDB.ui then return end
    GuildeMetiersSyncDB.ui.selectedPlayer = GMS.selectedPlayer
    GuildeMetiersSyncDB.ui.selectedProfession = GMS.selectedProfession
    if GMS.ui then
        if GMS.ui.searchBox then GuildeMetiersSyncDB.ui.memberSearch = GMS.ui.searchBox:GetText() or "" end
        if GMS.ui.recipeSearchBox then GuildeMetiersSyncDB.ui.recipeSearch = GMS.ui.recipeSearchBox:GetText() or "" end
    end
    saveWindowPosition()
end

local function restoreUIState()
    if not GuildeMetiersSyncDB.ui then return end
    GMS.selectedPlayer = GuildeMetiersSyncDB.ui.selectedPlayer
    GMS.selectedProfession = GuildeMetiersSyncDB.ui.selectedProfession
end

local function getOwnProfessions()
    local profs = {}
    local count = GetNumSkillLines() or 0

    for i = 1, count do
        local skillName, isHeader, _, skillRank, _, _, skillMaxRank = GetSkillLineInfo(i)
        if skillName and not isHeader and PRIMARY_PROFESSIONS[skillName] then
            table.insert(profs, {
                name = skillName or "",
                skill = skillRank or 0,
                maxSkill = skillMaxRank or 0,
            })
        end
    end

    table.sort(profs, function(a, b)
        return string.lower(a.name or "") < string.lower(b.name or "")
    end)

    return { profs[1], profs[2] }
end

local function updateOwnEntry()
    local db, key = getGuildDB()
    if not db then return end

    local name = UnitName("player")
    if not name or name == "" then return end

    local classLocalized, classFile = UnitClass("player")
    local level = UnitLevel("player") or 0
    local profs = getOwnProfessions()

    local entry = db.members[name] or {}
    entry.name = name
    entry.class = classFile or ""
    entry.classLocalized = classLocalized or ""
    entry.level = level
    entry.prof1 = profs[1] and profs[1].name or ""
    entry.prof1Skill = profs[1] and profs[1].skill or 0
    entry.prof1Max = profs[1] and profs[1].maxSkill or 0
    entry.prof2 = profs[2] and profs[2].name or ""
    entry.prof2Skill = profs[2] and profs[2].skill or 0
    entry.prof2Max = profs[2] and profs[2].maxSkill or 0
    entry.online = 1
    entry.lastSeen = time()
    entry.lastSync = time()
    entry.version = VERSION
    entry.guid = UnitGUID("player") or ""
    entry.guild = getGuildName()
    entry.recipes = entry.recipes or {}
    entry.hasAddon = true

    db.members[name] = entry
    db.updatedAt = time()

    GMS.guildKey = key
    GMS.data = db.members
end

local function serializeSelf()
    local db = getGuildDB()
    if not db then return nil end

    local me = db.members[UnitName("player")]
    if not me then return nil end

    return table.concat({
        "DATA",
        encodeField(me.name),
        encodeField(me.class),
        tostring(me.level or 0),
        encodeField(me.prof1),
        tostring(me.prof1Skill or 0),
        tostring(me.prof1Max or 0),
        encodeField(me.prof2),
        tostring(me.prof2Skill or 0),
        tostring(me.prof2Max or 0),
        tostring(me.lastSync or time()),
        encodeField(VERSION),
        encodeField(me.guild or ""),
    }, "|")
end

local function extractItemString(link)
    if not link or link == "" then return "" end
    local itemString = string.match(link, "(item:[%-%d:]+)")
    if itemString then return itemString end
    return ""
end

local function categorizeTailoring(name)
    local n = norm(name)
    if string.find(n, "sac", 1, true) or string.find(n, "bag", 1, true) then return "Sacs" end
    if string.find(n, "cape", 1, true) or string.find(n, "cloak", 1, true) then return "Dos" end
    if string.find(n, "robe", 1, true) or string.find(n, "tunique", 1, true) or string.find(n, "veste", 1, true) or string.find(n, "pourpoint", 1, true) or string.find(n, "chest", 1, true) then return "Torse" end
    if string.find(n, "chemise", 1, true) or string.find(n, "shirt", 1, true) then return "Chemises" end
    if string.find(n, "pantalon", 1, true) or string.find(n, "leggings", 1, true) or string.find(n, "leg", 1, true) or string.find(n, "pants", 1, true) then return "Jambes" end
    if string.find(n, "ceinture", 1, true) or string.find(n, "belt", 1, true) then return "Taille" end
    if string.find(n, "botte", 1, true) or string.find(n, "boots", 1, true) or string.find(n, "sandales", 1, true) or string.find(n, "slippers", 1, true) then return "Pieds" end
    if string.find(n, "gants", 1, true) or string.find(n, "gloves", 1, true) or string.find(n, "mitaines", 1, true) then return "Mains" end
    if string.find(n, "brassards", 1, true) or string.find(n, "wrist", 1, true) or string.find(n, "bracelet", 1, true) then return "Poignets" end
    if string.find(n, "epaule", 1, true) or string.find(n, "shoulder", 1, true) then return "Épaules" end
    if string.find(n, "capuche", 1, true) or string.find(n, "chapeau", 1, true) or string.find(n, "hat", 1, true) or string.find(n, "hood", 1, true) or string.find(n, "head", 1, true) then return "Tête" end
    if string.find(n, "fil", 1, true) or string.find(n, "bobine", 1, true) or string.find(n, "cloth", 1, true) or string.find(n, "etoffe", 1, true) or string.find(n, "rouleau", 1, true) or string.find(n, "bolt", 1, true) then return "Matériaux" end
    if string.find(n, "broderie", 1, true) or string.find(n, "embroidery", 1, true) then return "Améliorations" end
    return "Divers"
end

local function categorizeRecipe(skillName, recipeName, headerCategory)
    local prof = skillName or ""
    if prof == "Tailoring" or prof == "Couture" then
        return categorizeTailoring(recipeName)
    end
    if headerCategory and headerCategory ~= "" then
        return headerCategory
    end
    return "Autre"
end

local function serializeRecipe(recipe)
    local reagents = {}
    local list = recipe.reagents or {}
    for i = 1, #list do
        local reg = list[i]
        reagents[#reagents + 1] = table.concat({
            encodeField(reg.name or ""),
            tostring(reg.count or 0),
            tostring(reg.available or 0),
        }, "~")
    end

    return table.concat({
        encodeField(recipe.name or ""),
        tostring(recipe.numAvailable or 0),
        tostring(recipe.numMade or 0),
        encodeField(recipe.itemString or ""),
        encodeField(recipe.category or ""),
        join(reagents, "^"),
    }, "#")
end

local function deserializeRecipe(blob)
    local parts = split(blob, "#")
    local recipe = {
        name = parts[1] or "",
        numAvailable = tonumber(parts[2]) or 0,
        numMade = tonumber(parts[3]) or 0,
        itemString = parts[4] or "",
        category = parts[5] or "Autre",
        reagents = {},
    }

    local regs = split(parts[6] or "", "^")
    for i = 1, #regs do
        local rp = split(regs[i], "~")
        if rp[1] and rp[1] ~= "" then
            table.insert(recipe.reagents, {
                name = rp[1],
                count = tonumber(rp[2]) or 0,
                available = tonumber(rp[3]) or 0,
            })
        end
    end

    return recipe
end

local function sendAddon(text, channel, target)
    if not text or text == "" then return end
    if C_ChatInfo and C_ChatInfo.SendAddonMessage then
        C_ChatInfo.SendAddonMessage(PREFIX, text, channel, target)
    else
        SendAddonMessage(PREFIX, text, channel, target)
    end
end

local function buildRecipeChunks(playerName, profName, recipes)
    local chunks = {}
    if not profName or profName == "" then return chunks end
    recipes = recipes or {}

    local compact = {}
    for i = 1, math.min(#recipes, MAX_RECIPES_PER_PROF) do
        compact[#compact + 1] = serializeRecipe(recipes[i])
    end

    if #compact == 0 then
        chunks[1] = ""
        return chunks
    end

    local current = ""
    for i = 1, #compact do
        local piece = compact[i]
        local headerEstimate = string.len("RECIPES|" .. encodeField(playerName or "") .. "|" .. encodeField(profName) .. "|" .. tostring(time()) .. "|999|999|")
        local maxBlobLen = math.max(30, MAX_ADDON_MESSAGE_LEN - headerEstimate - 4)

        if string.len(piece) > maxBlobLen then
            piece = string.sub(piece, 1, maxBlobLen)
        end

        if current == "" then
            current = piece
        elseif (string.len(current) + 1 + string.len(piece)) <= maxBlobLen then
            current = current .. ";" .. piece
        else
            table.insert(chunks, current)
            current = piece
        end
    end

    if current ~= "" then
        table.insert(chunks, current)
    end

    return chunks
end

local function sendRecipeChunks(profName, recipes, channel, target)
    if not profName or profName == "" then return end
    local playerName = UnitName("player") or ""
    local chunks = buildRecipeChunks(playerName, profName, recipes)
    local syncTs = time()
    local total = #chunks > 0 and #chunks or 1

    for index = 1, total do
        local payload = table.concat({
            "RECIPES",
            encodeField(playerName),
            encodeField(profName),
            tostring(syncTs),
            tostring(index),
            tostring(total),
            chunks[index] or "",
        }, "|")
        sendAddon(payload, channel, target)
    end
end

local function sendStoredRecipes(channel, target)
    local db = getGuildDB()
    if not db then return end

    local me = db.members[UnitName("player")]
    if not me or not me.recipes then return end

    if me.prof1 and me.prof1 ~= "" and me.recipes[me.prof1] and me.recipes[me.prof1].list then
        sendRecipeChunks(me.prof1, me.recipes[me.prof1].list, channel, target)
    end

    if me.prof2 and me.prof2 ~= "" and me.recipes[me.prof2] and me.recipes[me.prof2].list then
        sendRecipeChunks(me.prof2, me.recipes[me.prof2].list, channel, target)
    end
end

local function broadcastSelfAndRecipes(channel, target)
    updateOwnEntry()
    local payload = serializeSelf()
    if payload then
        sendAddon(payload, channel or "GUILD", target)
    end
    sendStoredRecipes(channel or "GUILD", target)
end

local function requestSync(force)
    if not IsInGuild() then return end
    if not force and (GetTime() - (GMS.lastRequest or 0)) < 30 then return end

    local me = UnitName("player") or ""
    sendAddon("REQ|" .. encodeField(me), "GUILD")
    GMS.lastRequest = GetTime()
    msg("Demande de synchronisation envoyée.")
end

local function importEntry(parts, sender)
    local db = getGuildDB()
    if not db then return end

    local name = trimRealm(parts[2] or sender)
    if not name or name == "" then return end

    local entry = db.members[name] or {}
    entry.name = name
    entry.class = parts[3] or entry.class or ""
    entry.level = tonumber(parts[4]) or entry.level or 0
    entry.prof1 = parts[5] or ""
    entry.prof1Skill = tonumber(parts[6]) or 0
    entry.prof1Max = tonumber(parts[7]) or 0
    entry.prof2 = parts[8] or ""
    entry.prof2Skill = tonumber(parts[9]) or 0
    entry.prof2Max = tonumber(parts[10]) or 0
    entry.lastSync = tonumber(parts[11]) or time()
    entry.version = parts[12] or ""
    entry.guild = parts[13] or entry.guild or ""
    entry.online = 1
    entry.lastSeen = time()
    entry.recipes = entry.recipes or {}
    entry.hasAddon = true

    if entry.name ~= UnitName("player") then
        local total = GetNumGuildMembers() or 0
        for i = 1, total do
            local fullName, _, _, level, classDisplay, _, _, _, online, _, classFile = GetGuildRosterInfo(i)
            if trimRealm(fullName) == entry.name then
                entry.class = classFile or entry.class
                entry.classLocalized = classDisplay or entry.classLocalized
                entry.level = level or entry.level
                entry.online = online and 1 or 0
                break
            end
        end
    end

    db.members[name] = entry
    db.updatedAt = time()
    GMS.data = db.members
end

local function importRecipes(parts, sender)
    local db = getGuildDB()
    if not db then return end

    local playerName = trimRealm(parts[2] or sender)
    local profName = parts[3] or ""
    local syncTime = tonumber(parts[4]) or time()
    local partIndex = tonumber(parts[5]) or 1
    local partTotal = tonumber(parts[6]) or 1
    local recipeBlob = parts[7] or ""

    if playerName == "" or profName == "" then return end

    GMS.recipeInbox[playerName] = GMS.recipeInbox[playerName] or {}
    GMS.recipeInbox[playerName][profName] = GMS.recipeInbox[playerName][profName] or {}

    local inbox = GMS.recipeInbox[playerName][profName]

    if not inbox.syncTime or inbox.syncTime ~= syncTime then
        inbox.syncTime = syncTime
        inbox.total = partTotal
        inbox.parts = {}
    end

    inbox.total = partTotal
    inbox.parts[partIndex] = recipeBlob

    local received = 0
    for i = 1, partTotal do
        if inbox.parts[i] ~= nil then
            received = received + 1
        end
    end

    if received < partTotal then
        return
    end

    local mergedBlob = table.concat(inbox.parts, ";")
    local entry = db.members[playerName] or { name = playerName }
    entry.recipes = entry.recipes or {}
    entry.hasAddon = true
    entry.recipes[profName] = entry.recipes[profName] or {}

    local recipes = {}
    local rawList = split(mergedBlob, ";")
    for i = 1, #rawList do
        if rawList[i] and rawList[i] ~= "" then
            table.insert(recipes, deserializeRecipe(rawList[i]))
        end
    end

    entry.recipes[profName].list = recipes
    entry.recipes[profName].lastSync = syncTime

    db.members[playerName] = entry
    db.updatedAt = time()
    GMS.data = db.members

    GMS.recipeInbox[playerName][profName] = nil
end

local function updateRosterFlags()
    local db = getGuildDB()
    if not db then return end

    for _, entry in pairs(db.members) do
        entry.online = 0
    end

    local total = GetNumGuildMembers() or 0
    for i = 1, total do
        local fullName, _, _, level, classDisplay, _, _, _, online, _, classFile = GetGuildRosterInfo(i)
        local name = trimRealm(fullName)
        if name and name ~= "" then
            local entry = db.members[name]
            if entry then
                entry.class = classFile or entry.class or ""
                entry.classLocalized = classDisplay or entry.classLocalized or ""
                entry.level = level or entry.level or 0
                entry.online = online and 1 or 0
                if online then
                    entry.lastSeen = time()
                end
                entry.recipes = entry.recipes or {}
            end
        end
    end

    db.updatedAt = time()
    GMS.data = db.members
end

local function cleanupStaleData()
    local db = getGuildDB()
    if not db then return end
    local now = time()

    for name, entry in pairs(db.members) do
        if entry and entry.lastSync and (now - entry.lastSync) > (60 * 60 * 24 * 45) then
            db.members[name] = nil
        end
    end
end

local function formatProfession(name, skill, maxSkill)
    if not name or name == "" then
        return "-"
    end
    if skill and skill > 0 and maxSkill and maxSkill > 0 then
        return string.format("%s (%d/%d)", name, skill, maxSkill)
    end
    return name
end

local function hasAddonEntry(entry)
    return entry and entry.hasAddon and entry.version and entry.version ~= ""
end

local function matchesMemberFilter(entry, text)
    if not text or text == "" then return true end
    local hay = table.concat({
        norm(entry.name),
        norm(entry.classLocalized or entry.class or ""),
        norm(entry.prof1),
        norm(entry.prof2),
        norm(entry.version or ""),
    }, " ")
    return string.find(hay, norm(text), 1, true) ~= nil
end

local function compareEntries(a, b)
    local key = GMS.sortKey
    local av, bv

    if key == "class" then
        av = string.lower(a.classLocalized or a.class or "")
        bv = string.lower(b.classLocalized or b.class or "")
    elseif key == "level" then
        av = tonumber(a.level) or 0
        bv = tonumber(b.level) or 0
    else
        av = string.lower(a.name or "")
        bv = string.lower(b.name or "")
    end

    if av == bv then
        av = string.lower(a.name or "")
        bv = string.lower(b.name or "")
    end

    if GMS.sortAsc then
        return av < bv
    else
        return av > bv
    end
end

local function buildFilteredList()
    wipeTable(GMS.filtered)
    local text = GMS.ui and GMS.ui.searchBox and GMS.ui.searchBox:GetText() or ""

    for _, entry in pairs(GMS.data or {}) do
        if hasAddonEntry(entry) and matchesMemberFilter(entry, text) then
            table.insert(GMS.filtered, entry)
        end
    end

    table.sort(GMS.filtered, compareEntries)

    if not GMS.selectedPlayer or not GMS.data[GMS.selectedPlayer] or not hasAddonEntry(GMS.data[GMS.selectedPlayer]) then
        GMS.selectedPlayer = GMS.filtered[1] and GMS.filtered[1].name or nil
    end
end

local function buildRecipeList()
    wipeTable(GMS.recipeFiltered)

    if not GMS.selectedPlayer or not GMS.selectedProfession then
        return
    end

    local entry = GMS.data and GMS.data[GMS.selectedPlayer]
    if not entry or not entry.recipes or not entry.recipes[GMS.selectedProfession] or not entry.recipes[GMS.selectedProfession].list then
        return
    end

    local list = entry.recipes[GMS.selectedProfession].list
    local text = GMS.ui and GMS.ui.recipeSearchBox and GMS.ui.recipeSearchBox:GetText() or ""

    local raw = {}
    for i = 1, #list do
        local recipe = list[i]
        local okText = (text == "" or string.find(norm(recipe.name), norm(text), 1, true) ~= nil)
        if okText then
            table.insert(raw, recipe)
        end
    end

    table.sort(raw, function(a, b)
        local ac = string.lower(a.category or "autre")
        local bc = string.lower(b.category or "autre")
        if ac ~= bc then return ac < bc end
        return string.lower(a.name or "") < string.lower(b.name or "")
    end)

    local lastCategory = nil
    for i = 1, #raw do
        local recipe = raw[i]
        local cat = recipe.category or "Autre"
        if cat ~= lastCategory then
            table.insert(GMS.recipeFiltered, {
                _isHeader = true,
                category = cat,
                name = cat,
            })
            lastCategory = cat
        end
        table.insert(GMS.recipeFiltered, recipe)
    end

    if not GMS.selectedRecipeIndex or not GMS.recipeFiltered[GMS.selectedRecipeIndex] or GMS.recipeFiltered[GMS.selectedRecipeIndex]._isHeader then
        GMS.selectedRecipeIndex = nil
        for i = 1, #GMS.recipeFiltered do
            if not GMS.recipeFiltered[i]._isHeader then
                GMS.selectedRecipeIndex = i
                break
            end
        end
    end
end

local function getSelectedEntry()
    if not GMS.selectedPlayer then return nil end
    return GMS.data and GMS.data[GMS.selectedPlayer]
end

local function getSelectedRecipe()
    if not GMS.selectedRecipeIndex then return nil end
    local r = GMS.recipeFiltered and GMS.recipeFiltered[GMS.selectedRecipeIndex]
    if r and r._isHeader then return nil end
    return r
end

local function professionTooltip(owner, playerName, profName)
    GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
    GameTooltip:ClearLines()
    GameTooltip:AddLine(profName or "-", 1, 0.82, 0)
    GameTooltip:AddLine(playerName or "-", 1, 1, 1)

    local entry = GMS.data and GMS.data[playerName]
    if entry and entry.recipes and entry.recipes[profName] and entry.recipes[profName].list then
        local list = entry.recipes[profName].list
        GameTooltip:AddLine("Recettes synchronisées : " .. tostring(#list), 0.7, 0.9, 1)
        for i = 1, math.min(#list, 8) do
            GameTooltip:AddLine("- " .. (list[i].name or "?"), 0.85, 0.85, 0.85)
        end
        if #list > 8 then
            GameTooltip:AddLine("...", 0.6, 0.6, 0.6)
        end
    else
        GameTooltip:AddLine("Aucune recette synchronisée", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("Le joueur doit ouvrir sa fenêtre de métier au moins une fois.", 0.8, 0.6, 0.4)
    end
    GameTooltip:Show()
end

local function recipeTooltip(owner, recipe)
    if not recipe or recipe._isHeader then return end

    GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
    GameTooltip:ClearLines()

    local usedHyperlink = false
    if recipe.itemString and recipe.itemString ~= "" then
        local ok = pcall(GameTooltip.SetHyperlink, GameTooltip, recipe.itemString)
        if ok then
            usedHyperlink = true
        end
    end

    if not usedHyperlink then
        GameTooltip:AddLine(recipe.name or "-", 1, 0.82, 0)
    end

    GameTooltip:AddLine("Catégorie : " .. tostring(recipe.category or "Autre"), 0.8, 0.8, 0.9)
    GameTooltip:AddLine("Produit : x" .. tostring(recipe.numMade or 0), 0.8, 0.9, 1)

    if recipe.reagents and #recipe.reagents > 0 then
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("Composants", 1, 0.82, 0)
        for i = 1, #recipe.reagents do
            local reg = recipe.reagents[i]
            local enough = (reg.available or 0) >= (reg.count or 0)
            local line = "- " .. tostring(reg.name or "?") .. " x" .. tostring(reg.count or 0) .. " (" .. tostring(reg.available or 0) .. ")"
            if enough then
                GameTooltip:AddLine(line, 0.6, 1, 0.6)
            else
                GameTooltip:AddLine(line, 1, 0.45, 0.45)
            end
        end
    end

    GameTooltip:Show()
end

local function selectPlayer(name)
    GMS.selectedPlayer = name
    GMS.selectedRecipeIndex = nil

    local entry = GMS.data and GMS.data[name]
    if entry then
        if GMS.selectedProfession ~= entry.prof1 and GMS.selectedProfession ~= entry.prof2 then
            if entry.prof1 and entry.prof1 ~= "" then
                GMS.selectedProfession = entry.prof1
            elseif entry.prof2 and entry.prof2 ~= "" then
                GMS.selectedProfession = entry.prof2
            else
                GMS.selectedProfession = nil
            end
        end
    else
        GMS.selectedProfession = nil
    end

    buildRecipeList()
    saveUIState()
end

local function selectProfession(name)
    GMS.selectedProfession = name
    GMS.selectedRecipeIndex = nil
    buildRecipeList()
    saveUIState()
end

local function selectRecipeIndex(index)
    local entry = GMS.recipeFiltered[index]
    if entry and entry._isHeader then return end
    GMS.selectedRecipeIndex = index
    saveUIState()
end

local function setHeaderSort(key)
    if GMS.sortKey == key then
        GMS.sortAsc = not GMS.sortAsc
    else
        GMS.sortKey = key
        GMS.sortAsc = true
    end
    buildFilteredList()
    if GMS.ui and GMS.ui.frame and GMS.ui.frame:IsShown() then
        GMS:RefreshMemberList()
        GMS:RefreshCenterPanel()
        GMS:RefreshRecipePanel()
    end
end

local function toggleWindow()
    GMS:CreateUI()
    if GMS.ui.frame:IsShown() then
        GMS.ui.frame:Hide()
    else
        buildFilteredList()
        buildRecipeList()
        GMS:RefreshUI()
        GMS.ui.frame:Show()
        GMS.ui.frame:Raise()
    end
end

local function setMinimapVisible(show)
    ensureDB()
    GuildeMetiersSyncDB.minimap.hide = not show
    if GMS.minimapButton then
        if show then GMS.minimapButton:Show() else GMS.minimapButton:Hide() end
    end
end

local function createMinimapButton()
    if GMS.minimapButton then return end
    ensureDB()

    local b = CreateFrame("Button", "GMSMinimapButton", Minimap)
    b:SetSize(32, 32)
    b:SetFrameStrata("MEDIUM")
    b:SetFrameLevel(Minimap:GetFrameLevel() + 8)
    b:EnableMouse(true)
    b:SetMovable(true)

    local icon = b:CreateTexture(nil, "BACKGROUND")
    icon:SetTexture("Interface\\Icons\\INV_Scroll_11")
    icon:SetSize(20, 20)
    icon:SetPoint("CENTER")
    b.icon = icon

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

    b:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
    b:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    b:RegisterForDrag("LeftButton")

    local function updatePosition()
        local angle = GuildeMetiersSyncDB.minimap.angle or 45
        local rad = math.rad(angle)
        local x = math.cos(rad) * 78
        local y = math.sin(rad) * 78
        b:ClearAllPoints()
        b:SetPoint("CENTER", Minimap, "CENTER", x, y)
    end

    b:SetScript("OnClick", function(_, button)
        if button == "LeftButton" then
            toggleWindow()
        else
            requestSync(true)
        end
    end)

    b:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_LEFT")
        GameTooltip:AddLine("GuildeMetiersSync", 1, 0.82, 0)
        GameTooltip:AddLine("Clic gauche : ouvrir/fermer", 1, 1, 1)
        GameTooltip:AddLine("Clic droit : demander une synchro", 1, 1, 1)
        GameTooltip:Show()
    end)

    b:SetScript("OnLeave", function() GameTooltip:Hide() end)

    b:SetScript("OnDragStart", function(self)
        self:SetScript("OnUpdate", function(btn)
            local mx, my = GetCursorPosition()
            local scale = Minimap:GetEffectiveScale()
            mx = mx / scale
            my = my / scale

            local cx, cy = Minimap:GetCenter()
            local dx = mx - cx
            local dy = my - cy
            local angle = math.deg(math.atan2(dy, dx))
            GuildeMetiersSyncDB.minimap.angle = angle
            updatePosition()
        end)
    end)

    b:SetScript("OnDragStop", function(self)
        self:SetScript("OnUpdate", nil)
    end)

    GMS.minimapButton = b
    updatePosition()

    if GuildeMetiersSyncDB.minimap.hide then b:Hide() else b:Show() end
end

local function buildRecipeSignature(recipes)
    local tmp = {}
    for i = 1, #recipes do
        local r = recipes[i]
        tmp[#tmp + 1] = (r.name or "") .. ":" .. tostring(r.numMade or 0) .. ":" .. tostring(r.numAvailable or 0)
    end
    return table.concat(tmp, "|")
end

local function scanCurrentTradeSkill(forceSend)
    if not TradeSkillFrame or not TradeSkillFrame:IsShown() then return end

    local skillName = GetTradeSkillLine()
    if not skillName or skillName == "" or skillName == "UNKNOWN" then return end
    if not PRIMARY_PROFESSIONS[skillName] then return end

    local now = GetTime()
    if not forceSend and (now - (GMS.lastTradeScan or 0)) < TRADE_SCAN_THROTTLE then
        return
    end
    GMS.lastTradeScan = now

    local recipes = {}
    local count = GetNumTradeSkills() or 0
    local currentHeaderCategory = "Autre"

    for i = 1, count do
        local name, diff = GetTradeSkillInfo(i)

        if name and (diff == "header" or diff == "subheader") then
            currentHeaderCategory = name or "Autre"
        elseif name and name ~= "" then
            local itemLink = ""
            if GetTradeSkillItemLink then
                itemLink = GetTradeSkillItemLink(i) or ""
            end

            local recipe = {
                name = name,
                numAvailable = GetTradeSkillNumAvailable and (GetTradeSkillNumAvailable(i) or 0) or 0,
                numMade = GetTradeSkillNumMade and (GetTradeSkillNumMade(i) or 1) or 1,
                itemString = extractItemString(itemLink),
                category = categorizeRecipe(skillName, name, currentHeaderCategory),
                reagents = {},
            }

            local numRegs = GetTradeSkillNumReagents and (GetTradeSkillNumReagents(i) or 0) or 0
            for j = 1, numRegs do
                local reagentName, _, reagentCount, playerReagentCount = GetTradeSkillReagentInfo(i, j)
                if reagentName and reagentName ~= "" then
                    table.insert(recipe.reagents, {
                        name = reagentName,
                        count = reagentCount or 0,
                        available = playerReagentCount or 0,
                    })
                end
            end

            table.insert(recipes, recipe)
        end
    end

    local signature = buildRecipeSignature(recipes)
    if not forceSend and GMS.lastTradeSignatureByProf[skillName] == signature then
        return
    end
    GMS.lastTradeSignatureByProf[skillName] = signature

    local me = UnitName("player")
    local entry = GMS.data and GMS.data[me]
    if not entry then
        updateOwnEntry()
        entry = GMS.data and GMS.data[me]
    end
    if not entry then return end

    entry.recipes = entry.recipes or {}
    entry.recipes[skillName] = {
        list = recipes,
        lastSync = time(),
    }

    if (GetTime() - (GMS.lastBroadcast or 0)) > 1.5 then
        sendRecipeChunks(skillName, recipes, "GUILD")
        GMS.lastBroadcast = GetTime()
    end

    if GMS.selectedPlayer == me and GMS.selectedProfession == skillName and GMS.ui and GMS.ui.frame and GMS.ui.frame:IsShown() then
        buildRecipeList()
        GMS:RefreshRecipePanel()
        GMS:RefreshCenterPanel()
    end
end

function GMS:CreateUI()
    if self.ui then return end

    local f = CreateFrame("Frame", "GuildeMetiersSyncMainFrame", UIParent)
    f:SetSize(1110, 640)
    f:SetFrameStrata("DIALOG")
    f:EnableMouse(true)
    f:SetMovable(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", function(self) self:StartMoving() end)
    f:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        saveWindowPosition()
    end)
    createBackdrop(f)
    restoreWindowPosition(f)
    f:Hide()

    table.insert(UISpecialFrames, "GuildeMetiersSyncMainFrame")

    local bg = f:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetTexture("Interface\\Buttons\\WHITE8X8")
    bg:SetVertexColor(0.02, 0.02, 0.03, 1)

    local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", -6, -6)

    local guildTitle = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    guildTitle:SetPoint("TOPLEFT", 16, -12)
    guildTitle:SetText(getGuildName())
    f.guildTitle = guildTitle

    local subtitle = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    subtitle:SetPoint("TOPLEFT", guildTitle, "BOTTOMLEFT", 0, -4)
    subtitle:SetText("|cffffd200GuildeMetiersSync|r")

    local subtitleInfo = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    subtitleInfo:SetPoint("TOPLEFT", subtitle, "BOTTOMLEFT", 0, -4)
    subtitleInfo:SetWidth(760)
    subtitleInfo:SetJustifyH("LEFT")
    subtitleInfo:SetTextColor(0.92, 0.92, 0.92)
    subtitleInfo:SetText("Annuaire des membres ayant l'addon, avec leurs métiers et leurs recettes synchronisées. Ouvre ta fenêtre de métier pour mettre à jour tes crafts.")

    local syncBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    syncBtn:SetSize(110, 24)
    syncBtn:SetPoint("TOPRIGHT", -126, -14)
    syncBtn:SetText("Synchroniser")
    syncBtn:SetScript("OnClick", function()
        GuildRoster()
        updateOwnEntry()
        broadcastSelfAndRecipes("GUILD")
        requestSync(true)
        buildFilteredList()
        buildRecipeList()
        GMS:RefreshUI()
    end)

    local meBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    meBtn:SetSize(90, 24)
    meBtn:SetPoint("LEFT", syncBtn, "RIGHT", 8, 0)
    meBtn:SetText("Maj moi")
    meBtn:SetScript("OnClick", function()
        updateOwnEntry()
        broadcastSelfAndRecipes("GUILD")
        buildFilteredList()
        buildRecipeList()
        if GMS.ui and GMS.ui.frame and GMS.ui.frame:IsShown() then
            GMS:RefreshUI()
        end
    end)

    local directory = CreateFrame("Frame", nil, f)
    directory:SetPoint("TOPLEFT", 10, -92)
    directory:SetPoint("BOTTOMRIGHT", -10, 10)

    local leftPanel = createPanel(directory, 0, 0, -780, 0)
    local centerPanel = createPanel(directory, 300, 0, -390, 0)
    local rightPanel = createPanel(directory, 680, 0, 0, 0)

    local searchLabel = leftPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    searchLabel:SetPoint("TOPLEFT", 12, -12)
    searchLabel:SetText("Joueurs GMS")

    local searchBox = createEditBox(leftPanel, 178, 22)
    searchBox:SetPoint("TOPRIGHT", -12, -10)
    searchBox:SetScript("OnTextChanged", function(self, userInput)
        buildFilteredList()
        if not GMS.selectedPlayer and GMS.filtered[1] then
            selectPlayer(GMS.filtered[1].name)
        elseif GMS.selectedPlayer and GMS.data[GMS.selectedPlayer] and not hasAddonEntry(GMS.data[GMS.selectedPlayer]) then
            selectPlayer(GMS.filtered[1] and GMS.filtered[1].name or nil)
        end
        buildRecipeList()
        saveUIState()
        if GMS.ui and GMS.ui.frame and GMS.ui.frame:IsShown() then
            GMS:RefreshMemberList()
            GMS:RefreshCenterPanel()
            GMS:RefreshRecipePanel()
        end
        if userInput then
            self:SetFocus()
        end
    end)

    local headerName = CreateFrame("Button", nil, leftPanel)
    headerName:SetPoint("TOPLEFT", 12, -42)
    headerName:SetSize(146, 18)
    headerName:SetScript("OnClick", function() setHeaderSort("name") end)
    local headerNameText = headerName:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    headerNameText:SetAllPoints()
    headerNameText:SetJustifyH("LEFT")
    headerNameText:SetText("Nom")

    local headerLevel = CreateFrame("Button", nil, leftPanel)
    headerLevel:SetPoint("TOPLEFT", 162, -42)
    headerLevel:SetSize(36, 18)
    headerLevel:SetScript("OnClick", function() setHeaderSort("level") end)
    local headerLevelText = headerLevel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    headerLevelText:SetAllPoints()
    headerLevelText:SetJustifyH("LEFT")
    headerLevelText:SetText("Niv")

    local headerClass = CreateFrame("Button", nil, leftPanel)
    headerClass:SetPoint("TOPLEFT", 206, -42)
    headerClass:SetSize(72, 18)
    headerClass:SetScript("OnClick", function() setHeaderSort("class") end)
    local headerClassText = headerClass:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    headerClassText:SetAllPoints()
    headerClassText:SetJustifyH("LEFT")
    headerClassText:SetText("Classe")

    local memberRows = {}
    local memberRowHeight = 50

    for i = 1, 10 do
        local row = CreateFrame("Button", nil, leftPanel)
        row:SetSize(286, memberRowHeight)
        row:SetPoint("TOPLEFT", 8, -62 - ((i - 1) * memberRowHeight))

        row.bg = row:CreateTexture(nil, "BACKGROUND")
        row.bg:SetAllPoints()
        row.bg:SetTexture("Interface\\Buttons\\WHITE8X8")
        row.bg:SetVertexColor(0.05, 0.06, 0.09, 0.98)

        row.sel = row:CreateTexture(nil, "BORDER")
        row.sel:SetAllPoints()
        row.sel:SetTexture("Interface\\Buttons\\WHITE8X8")
        row.sel:SetVertexColor(0.18, 0.31, 0.53, 0.95)
        row.sel:Hide()

        row.hl = row:CreateTexture(nil, "HIGHLIGHT")
        row.hl:SetAllPoints()
        row.hl:SetTexture("Interface\\Buttons\\WHITE8X8")
        row.hl:SetVertexColor(1, 1, 1, 0.06)

        row.leftAccent = row:CreateTexture(nil, "ARTWORK")
        row.leftAccent:SetPoint("TOPLEFT", 0, 0)
        row.leftAccent:SetPoint("BOTTOMLEFT", 0, 0)
        row.leftAccent:SetWidth(4)
        row.leftAccent:SetTexture("Interface\\Buttons\\WHITE8X8")
        row.leftAccent:SetVertexColor(0.25, 0.65, 1, 1)

        row.statusDot = createStatusDot(row, 10)
        row.statusDot:SetPoint("TOPLEFT", 7, -7)

        row.classIcon = row:CreateTexture(nil, "ARTWORK")
        row.classIcon:SetSize(18, 18)
        row.classIcon:SetPoint("TOPLEFT", 20, -5)
        row.classIcon:SetTexture("Interface\\Glues\\CharacterCreate\\UI-CharacterCreate-Classes")

        row.name = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        row.name:SetPoint("TOPLEFT", 42, -5)
        row.name:SetWidth(103)
        row.name:SetJustifyH("LEFT")

        row.level = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        row.level:SetPoint("TOPLEFT", 149, -5)
        row.level:SetWidth(30)
        row.level:SetJustifyH("LEFT")

        row.class = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        row.class:SetPoint("TOPLEFT", 183, -5)
        row.class:SetWidth(98)
        row.class:SetJustifyH("LEFT")

        row.prof1Icon = row:CreateTexture(nil, "ARTWORK")
        row.prof1Icon:SetSize(14, 14)
        row.prof1Icon:SetPoint("TOPLEFT", 8, -27)

        row.prof1Text = row:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
        row.prof1Text:SetPoint("LEFT", row.prof1Icon, "RIGHT", 4, 0)
        row.prof1Text:SetWidth(112)
        row.prof1Text:SetJustifyH("LEFT")

        row.prof2Icon = row:CreateTexture(nil, "ARTWORK")
        row.prof2Icon:SetSize(14, 14)
        row.prof2Icon:SetPoint("TOPLEFT", 126, -27)

        row.prof2Text = row:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
        row.prof2Text:SetPoint("LEFT", row.prof2Icon, "RIGHT", 4, 0)
        row.prof2Text:SetWidth(95)
        row.prof2Text:SetJustifyH("LEFT")

        row.meta = row:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
        row.meta:SetPoint("TOPRIGHT", -6, -27)
        row.meta:SetWidth(80)
        row.meta:SetJustifyH("RIGHT")

        row.separator = row:CreateTexture(nil, "ARTWORK")
        row.separator:SetPoint("BOTTOMLEFT", 6, 0)
        row.separator:SetPoint("BOTTOMRIGHT", -6, 0)
        row.separator:SetHeight(1)
        row.separator:SetTexture("Interface\\Buttons\\WHITE8X8")
        row.separator:SetVertexColor(0.18, 0.18, 0.24, 1)

        row:SetScript("OnClick", function(self)
            if self.playerName then
                selectPlayer(self.playerName)
                if GMS.ui and GMS.ui.frame and GMS.ui.frame:IsShown() then
                    GMS:RefreshMemberList()
                    GMS:RefreshCenterPanel()
                    GMS:RefreshRecipePanel()
                end
            end
        end)

        memberRows[i] = row
    end

    local memberScroll = CreateFrame("ScrollFrame", "GMSMemberScrollFrame", leftPanel, "FauxScrollFrameTemplate")
    memberScroll:SetPoint("TOPLEFT", 0, -58)
    memberScroll:SetPoint("BOTTOMRIGHT", -2, 6)
    memberScroll:SetScript("OnVerticalScroll", function(self, offset)
        FauxScrollFrame_OnVerticalScroll(self, offset, memberRowHeight, function()
            GMS:RefreshMemberList()
        end)
    end)

    local portraitBg = CreateFrame("Frame", nil, centerPanel)
    portraitBg:SetSize(82, 82)
    portraitBg:SetPoint("TOPLEFT", 14, -14)
    createBackdrop(portraitBg)

    local portrait = portraitBg:CreateTexture(nil, "ARTWORK")
    portrait:SetPoint("TOPLEFT", 4, -4)
    portrait:SetPoint("BOTTOMRIGHT", -4, 4)
    portrait:SetTexture("Interface\\Glues\\CharacterCreate\\UI-CharacterCreate-Classes")

    local playerNameText = centerPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    playerNameText:SetPoint("TOPLEFT", portraitBg, "TOPRIGHT", 12, -2)
    playerNameText:SetWidth(210)
    playerNameText:SetJustifyH("LEFT")
    playerNameText:SetText("-")

    local playerMetaText = centerPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    playerMetaText:SetPoint("TOPLEFT", playerNameText, "BOTTOMLEFT", 0, -6)
    playerMetaText:SetWidth(250)
    playerMetaText:SetJustifyH("LEFT")
    playerMetaText:SetText("-")

    local playerSyncText = centerPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    playerSyncText:SetPoint("TOPLEFT", playerMetaText, "BOTTOMLEFT", 0, -6)
    playerSyncText:SetWidth(250)
    playerSyncText:SetJustifyH("LEFT")
    playerSyncText:SetText("-")

    local whisperBtn = CreateFrame("Button", nil, centerPanel, "UIPanelButtonTemplate")
    whisperBtn:SetSize(80, 22)
    whisperBtn:SetPoint("TOPLEFT", 14, -108)
    whisperBtn:SetText("Whisper")
    whisperBtn:SetScript("OnClick", function()
        local entry = getSelectedEntry()
        if entry and entry.name and entry.name ~= "" then
            ChatFrame_SendTell(entry.name)
        end
    end)

    local syncPlayerBtn = CreateFrame("Button", nil, centerPanel, "UIPanelButtonTemplate")
    syncPlayerBtn:SetSize(120, 22)
    syncPlayerBtn:SetPoint("LEFT", whisperBtn, "RIGHT", 8, 0)
    syncPlayerBtn:SetText("Demander sync")
    syncPlayerBtn:SetScript("OnClick", function()
        local entry = getSelectedEntry()
        if not entry or not entry.name or entry.name == "" then return end
        sendAddon("REQ|" .. encodeField(UnitName("player") or ""), "WHISPER", entry.name)
        msg("Demande envoyée à " .. entry.name)
    end)

    local prof1Btn = CreateFrame("Button", nil, centerPanel)
    prof1Btn:SetSize(270, 42)
    prof1Btn:SetPoint("TOPLEFT", 14, -144)
    createBackdrop(prof1Btn)
    prof1Btn.icon = prof1Btn:CreateTexture(nil, "ARTWORK")
    prof1Btn.icon:SetSize(22, 22)
    prof1Btn.icon:SetPoint("LEFT", 10, 0)
    prof1Btn.text = prof1Btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    prof1Btn.text:SetPoint("LEFT", prof1Btn.icon, "RIGHT", 10, 0)
    prof1Btn.text:SetWidth(210)
    prof1Btn.text:SetJustifyH("LEFT")
    prof1Btn.active = prof1Btn:CreateTexture(nil, "BORDER")
    prof1Btn.active:SetAllPoints()
    prof1Btn.active:SetTexture("Interface\\Buttons\\WHITE8X8")
    prof1Btn.active:SetVertexColor(0.16, 0.28, 0.48, 0.90)
    prof1Btn.active:Hide()

    local prof2Btn = CreateFrame("Button", nil, centerPanel)
    prof2Btn:SetSize(270, 42)
    prof2Btn:SetPoint("TOPLEFT", 14, -194)
    createBackdrop(prof2Btn)
    prof2Btn.icon = prof2Btn:CreateTexture(nil, "ARTWORK")
    prof2Btn.icon:SetSize(22, 22)
    prof2Btn.icon:SetPoint("LEFT", 10, 0)
    prof2Btn.text = prof2Btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    prof2Btn.text:SetPoint("LEFT", prof2Btn.icon, "RIGHT", 10, 0)
    prof2Btn.text:SetWidth(210)
    prof2Btn.text:SetJustifyH("LEFT")
    prof2Btn.active = prof2Btn:CreateTexture(nil, "BORDER")
    prof2Btn.active:SetAllPoints()
    prof2Btn.active:SetTexture("Interface\\Buttons\\WHITE8X8")
    prof2Btn.active:SetVertexColor(0.16, 0.28, 0.48, 0.90)
    prof2Btn.active:Hide()

    prof1Btn:SetScript("OnClick", function(self)
        if self.profName and self.profName ~= "" then
            selectProfession(self.profName)
            GMS:RefreshRecipePanel()
            GMS:RefreshCenterPanel()
        end
    end)
    prof2Btn:SetScript("OnClick", function(self)
        if self.profName and self.profName ~= "" then
            selectProfession(self.profName)
            GMS:RefreshRecipePanel()
            GMS:RefreshCenterPanel()
        end
    end)
    prof1Btn:SetScript("OnEnter", function(self)
        if self.playerName and self.profName and self.profName ~= "" then
            professionTooltip(self, self.playerName, self.profName)
        end
    end)
    prof2Btn:SetScript("OnEnter", function(self)
        if self.playerName and self.profName and self.profName ~= "" then
            professionTooltip(self, self.playerName, self.profName)
        end
    end)
    prof1Btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    prof2Btn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    local statsText = centerPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    statsText:SetPoint("TOPLEFT", 14, -244)
    statsText:SetWidth(280)
    statsText:SetJustifyH("LEFT")
    statsText:SetJustifyV("TOP")
    statsText:SetText("-")

    local centerArt = centerPanel:CreateTexture(nil, "BACKGROUND")
    centerArt:SetPoint("TOPLEFT", 14, -356)
    centerArt:SetPoint("BOTTOMRIGHT", -14, 14)
    centerArt:SetTexture(CUSTOM_CENTER_ART)
    centerArt:SetTexCoord(0, 1, 0, 1)
    centerArt:SetVertexColor(1, 1, 1, 1)

    local recipeTitle = rightPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    recipeTitle:SetPoint("TOPLEFT", 12, -12)
    recipeTitle:SetWidth(190)
    recipeTitle:SetJustifyH("LEFT")
    recipeTitle:SetText("Recettes")

    local recipeSearchBox = createEditBox(rightPanel, 146, 22)
    recipeSearchBox:SetPoint("TOPRIGHT", -12, -10)
    recipeSearchBox:SetScript("OnTextChanged", function(self, userInput)
        buildRecipeList()
        saveUIState()
        if GMS.ui and GMS.ui.frame and GMS.ui.frame:IsShown() then
            GMS:RefreshRecipePanel()
        end
        if userInput then
            self:SetFocus()
        end
    end)

    local recipeListBg = CreateFrame("Frame", nil, rightPanel)
    recipeListBg:SetPoint("TOPLEFT", 10, -42)
    recipeListBg:SetPoint("TOPRIGHT", -10, -42)
    recipeListBg:SetHeight(250)
    createBackdrop(recipeListBg)

    local recipeRows = {}
    local recipeRowHeight = 28

    for i = 1, 8 do
        local row = CreateFrame("Button", nil, recipeListBg)
        row:SetWidth(384)
        row:SetHeight(recipeRowHeight)
        row:SetPoint("TOPLEFT", 8, -8 - ((i - 1) * recipeRowHeight))

        row.bg = row:CreateTexture(nil, "BACKGROUND")
        row.bg:SetAllPoints()
        row.bg:SetTexture("Interface\\Buttons\\WHITE8X8")
        row.bg:SetVertexColor(0.08, 0.08, 0.10, 1)

        row.sel = row:CreateTexture(nil, "BORDER")
        row.sel:SetAllPoints()
        row.sel:SetTexture("Interface\\Buttons\\WHITE8X8")
        row.sel:SetVertexColor(0.16, 0.28, 0.48, 0.90)
        row.sel:Hide()

        row.hl = row:CreateTexture(nil, "HIGHLIGHT")
        row.hl:SetAllPoints()
        row.hl:SetTexture("Interface\\Buttons\\WHITE8X8")
        row.hl:SetVertexColor(1, 1, 1, 0.08)

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

        row.name = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        row.name:SetPoint("LEFT", 28, 0)
        row.name:SetWidth(228)
        row.name:SetJustifyH("LEFT")

        row.meta = row:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
        row.meta:SetPoint("LEFT", 262, 0)
        row.meta:SetWidth(88)
        row.meta:SetJustifyH("LEFT")

        row.count = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        row.count:SetPoint("RIGHT", -4, 0)
        row.count:SetWidth(24)
        row.count:SetJustifyH("RIGHT")

        row:SetScript("OnClick", function(self)
            if self.recipeIndex then
                selectRecipeIndex(self.recipeIndex)
                GMS:RefreshRecipePanel()
            end
        end)
        row:SetScript("OnEnter", function(self)
            if self.recipeData and not self.recipeData._isHeader then
                recipeTooltip(self, self.recipeData)
            end
        end)
        row:SetScript("OnLeave", function() GameTooltip:Hide() end)

        recipeRows[i] = row
    end

    local recipeScroll = CreateFrame("ScrollFrame", "GMSRecipeScrollFrame", recipeListBg, "FauxScrollFrameTemplate")
    recipeScroll:SetPoint("TOPLEFT", 0, -4)
    recipeScroll:SetPoint("BOTTOMRIGHT", -2, 4)
    recipeScroll:SetScript("OnVerticalScroll", function(self, offset)
        FauxScrollFrame_OnVerticalScroll(self, offset, recipeRowHeight, function()
            GMS:RefreshRecipeList()
        end)
    end)

    local recipeDetailBg = CreateFrame("Frame", nil, rightPanel)
    recipeDetailBg:SetPoint("TOPLEFT", 10, -302)
    recipeDetailBg:SetPoint("BOTTOMRIGHT", -10, 10)
    createBackdrop(recipeDetailBg)

    local detailIcon = recipeDetailBg:CreateTexture(nil, "ARTWORK")
    detailIcon:SetSize(36, 36)
    detailIcon:SetPoint("TOPLEFT", 10, -10)
    detailIcon:SetTexture(DEFAULT_RECIPE_ICON)

    local detailName = recipeDetailBg:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    detailName:SetPoint("TOPLEFT", detailIcon, "TOPRIGHT", 10, -2)
    detailName:SetWidth(320)
    detailName:SetJustifyH("LEFT")
    detailName:SetText("-")

    local detailMeta = recipeDetailBg:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    detailMeta:SetPoint("TOPLEFT", detailName, "BOTTOMLEFT", 0, -6)
    detailMeta:SetWidth(320)
    detailMeta:SetJustifyH("LEFT")
    detailMeta:SetText("-")

    local detailReagents = recipeDetailBg:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    detailReagents:SetPoint("TOPLEFT", 10, -60)
    detailReagents:SetPoint("BOTTOMRIGHT", -10, 10)
    detailReagents:SetJustifyH("LEFT")
    detailReagents:SetJustifyV("TOP")
    detailReagents:SetText("Sélectionne une recette.")

    self.ui = {
        frame = f,
        guildTitle = guildTitle,
        subtitleInfo = subtitleInfo,

        directory = directory,

        searchBox = searchBox,
        memberRows = memberRows,
        memberScroll = memberScroll,
        memberRowHeight = memberRowHeight,

        portrait = portrait,
        playerNameText = playerNameText,
        playerMetaText = playerMetaText,
        playerSyncText = playerSyncText,
        whisperBtn = whisperBtn,
        syncPlayerBtn = syncPlayerBtn,
        prof1Btn = prof1Btn,
        prof2Btn = prof2Btn,
        statsText = statsText,
        centerArt = centerArt,

        recipeTitle = recipeTitle,
        recipeSearchBox = recipeSearchBox,
        recipeRows = recipeRows,
        recipeScroll = recipeScroll,
        recipeRowHeight = recipeRowHeight,
        detailIcon = detailIcon,
        detailName = detailName,
        detailMeta = detailMeta,
        detailReagents = detailReagents,
    }

    if GuildeMetiersSyncDB.ui.memberSearch and searchBox then
        searchBox:SetText(GuildeMetiersSyncDB.ui.memberSearch or "")
    end
    if GuildeMetiersSyncDB.ui.recipeSearch and recipeSearchBox then
        recipeSearchBox:SetText(GuildeMetiersSyncDB.ui.recipeSearch or "")
    end
end

function GMS:RefreshMemberList()
    if not self.ui then return end

    buildFilteredList()

    local rows = self.ui.memberRows
    local offset = FauxScrollFrame_GetOffset(self.ui.memberScroll)
    local total = #self.filtered

    FauxScrollFrame_Update(self.ui.memberScroll, total, #rows, self.ui.memberRowHeight)

    for i = 1, #rows do
        local row = rows[i]
        local entry = self.filtered[i + offset]

        if entry then
            local r, g, b = getClassColor(entry.class)
            row.playerName = entry.name

            row.name:SetText(truncateText(entry.name or "", 14))
            row.name:SetTextColor(r, g, b)

            row.level:SetText(tostring(entry.level or ""))
            row.level:SetTextColor(1, 0.82, 0.2)

            row.class:SetText(truncateText(entry.classLocalized or entry.class or "-", 11))
            row.class:SetTextColor(r, g, b)

            if entry.online == 1 then
                row.statusDot:SetTexture(ONLINE_TEX)
                row.statusDot:SetVertexColor(1, 1, 1, 1)
            else
                row.statusDot:SetTexture(OFFLINE_TEX)
                row.statusDot:SetVertexColor(1, 1, 1, 1)
            end

            if CLASS_ICON_TCOORDS and entry.class and CLASS_ICON_TCOORDS[entry.class] then
                local coords = CLASS_ICON_TCOORDS[entry.class]
                row.classIcon:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
                row.classIcon:SetAlpha(1)
            else
                row.classIcon:SetTexCoord(0, 1, 0, 1)
                row.classIcon:SetAlpha(0.35)
            end

            if entry.prof1 and entry.prof1 ~= "" then
                row.prof1Icon:SetTexture(getProfessionIcon(entry.prof1))
                row.prof1Icon:Show()
                row.prof1Text:SetText(truncateText(entry.prof1, 12))
                local pr, pg, pb = getProfessionColor(entry.prof1)
                row.prof1Text:SetTextColor(pr, pg, pb)
            else
                row.prof1Icon:Hide()
                row.prof1Text:SetText("-")
                row.prof1Text:SetTextColor(0.55, 0.55, 0.55)
            end

            if entry.prof2 and entry.prof2 ~= "" then
                row.prof2Icon:SetTexture(getProfessionIcon(entry.prof2))
                row.prof2Icon:Show()
                row.prof2Text:SetText(truncateText(entry.prof2, 11))
                local pr2, pg2, pb2 = getProfessionColor(entry.prof2)
                row.prof2Text:SetTextColor(pr2, pg2, pb2)
            else
                row.prof2Icon:Hide()
                row.prof2Text:SetText("-")
                row.prof2Text:SetTextColor(0.55, 0.55, 0.55)
            end

            local offlineTag = entry.online == 1 and "" or " offline"
            row.meta:SetText("V5" .. offlineTag)
            row.meta:SetTextColor(0.65, 0.65, 0.72)

            if self.selectedPlayer == entry.name then
                row.sel:Show()
                row.leftAccent:SetVertexColor(0.35, 0.75, 1, 1)
            else
                row.sel:Hide()
                row.leftAccent:SetVertexColor(0.16, 0.35, 0.55, 1)
            end

            row:Show()
        else
            row.playerName = nil
            row:Hide()
        end
    end
end

function GMS:RefreshCenterPanel()
    if not self.ui then return end

    local entry = getSelectedEntry()
    if not entry then
        self.ui.playerNameText:SetText("Aucun joueur")
        self.ui.playerMetaText:SetText("-")
        self.ui.playerSyncText:SetText("-")
        self.ui.statsText:SetText("Sélectionne un membre dans la liste.")
        self.ui.prof1Btn:Hide()
        self.ui.prof2Btn:Hide()
        self.ui.portrait:SetTexCoord(0, 1, 0, 1)
        self.ui.portrait:SetAlpha(0.35)
        return
    end

    local r, g, b = getClassColor(entry.class)
    self.ui.playerNameText:SetText(entry.name or "-")
    self.ui.playerNameText:SetTextColor(r, g, b)

    local onlineText = entry.online == 1 and "|cff55ff55En ligne|r" or "|cff7fa6ffHors ligne|r"
    self.ui.playerMetaText:SetText((entry.classLocalized or entry.class or "-") .. " • Niveau " .. tostring(entry.level or 0) .. " • " .. onlineText)
    self.ui.playerSyncText:SetText("Dernière synchro : " .. formatTimeAgo(entry.lastSync))

    if CLASS_ICON_TCOORDS and entry.class and CLASS_ICON_TCOORDS[entry.class] then
        local coords = CLASS_ICON_TCOORDS[entry.class]
        self.ui.portrait:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
        self.ui.portrait:SetAlpha(1)
    else
        self.ui.portrait:SetTexCoord(0, 1, 0, 1)
        self.ui.portrait:SetAlpha(0.35)
    end

    local function recipeCountFor(prof)
        if not prof or prof == "" then return 0 end
        if entry.recipes and entry.recipes[prof] and entry.recipes[prof].list then
            return #entry.recipes[prof].list
        end
        return 0
    end

    self.ui.statsText:SetText(
        "• Guilde : " .. tostring(entry.guild or "-") .. "\n" ..
        "• Version : V5\n" ..
        "• " .. tostring(entry.prof1 ~= "" and entry.prof1 or "Métier 1") .. " : " .. tostring(recipeCountFor(entry.prof1)) .. " recettes\n" ..
        "• " .. tostring(entry.prof2 ~= "" and entry.prof2 or "Métier 2") .. " : " .. tostring(recipeCountFor(entry.prof2)) .. " recettes"
    )

    local function setupProfButton(btn, profName, skill, maxSkill)
        if profName and profName ~= "" then
            local pr, pg, pb = getProfessionColor(profName)
            btn.profName = profName
            btn.playerName = entry.name
            btn.icon:SetTexture(getProfessionIcon(profName))
            btn.text:SetText(formatProfession(profName, skill, maxSkill))
            btn.text:SetTextColor(pr, pg, pb)
            if GMS.selectedProfession == profName then btn.active:Show() else btn.active:Hide() end
            btn:Show()
        else
            btn.profName = nil
            btn.playerName = nil
            btn:Hide()
        end
    end

    setupProfButton(self.ui.prof1Btn, entry.prof1, entry.prof1Skill, entry.prof1Max)
    setupProfButton(self.ui.prof2Btn, entry.prof2, entry.prof2Skill, entry.prof2Max)
end

function GMS:RefreshRecipeList()
    if not self.ui then return end

    buildRecipeList()

    local rows = self.ui.recipeRows
    local offset = FauxScrollFrame_GetOffset(self.ui.recipeScroll)
    local total = #self.recipeFiltered

    FauxScrollFrame_Update(self.ui.recipeScroll, total, #rows, self.ui.recipeRowHeight)

    for i = 1, #rows do
        local row = rows[i]
        local recipe = self.recipeFiltered[i + offset]

        if recipe then
            row.recipeData = recipe
            row.recipeIndex = i + offset

            if recipe._isHeader then
                row.icon:SetTexture("")
                row.name:SetText("[" .. tostring(recipe.category or "Autre") .. "]")
                row.name:SetTextColor(1, 0.82, 0)
                row.meta:SetText("")
                row.count:SetText("")
                row.sel:Hide()
            else
                row.icon:SetTexture(getRecipeIcon(recipe))
                row.name:SetText(truncateText(recipe.name or "?", 30))
                row.meta:SetText(truncateText(recipe.category or "Autre", 12))
                row.meta:SetTextColor(0.70, 0.70, 0.70)
                row.name:SetTextColor(0.85, 0.85, 0.85)
                row.count:SetText("")
                if self.selectedRecipeIndex == (i + offset) then row.sel:Show() else row.sel:Hide() end
            end

            row:Show()
        else
            row.recipeData = nil
            row.recipeIndex = nil
            row:Hide()
        end
    end
end

function GMS:RefreshRecipeDetail()
    if not self.ui then return end

    local entry = getSelectedEntry()
    local recipe = getSelectedRecipe()

    if not entry or not self.selectedProfession or self.selectedProfession == "" then
        self.ui.recipeTitle:SetText("Recettes")
        self.ui.detailIcon:SetTexture(DEFAULT_RECIPE_ICON)
        self.ui.detailName:SetText("Aucun métier sélectionné")
        self.ui.detailMeta:SetText("-")
        self.ui.detailReagents:SetText("Sélectionne un joueur puis un métier.")
        return
    end

    self.ui.recipeTitle:SetText("Recettes - " .. truncateText(tostring(self.selectedProfession), 22))

    if not recipe then
        self.ui.detailIcon:SetTexture(DEFAULT_RECIPE_ICON)
        self.ui.detailName:SetText("Aucune recette")
        self.ui.detailMeta:SetText("Aucune recette synchronisée ou aucun résultat.")
        self.ui.detailReagents:SetText("Ouvre la fenêtre de métier du joueur concerné pour synchroniser.")
        return
    end

    self.ui.detailIcon:SetTexture(getRecipeIcon(recipe))
    self.ui.detailName:SetText(recipe.name or "-")
    self.ui.detailMeta:SetText("Catégorie : " .. tostring(recipe.category or "Autre") .. " • Produit : x" .. tostring(recipe.numMade or 0))

    local lines = {}
    if recipe.reagents and #recipe.reagents > 0 then
        lines[#lines + 1] = "|cffffd200Composants|r"
        lines[#lines + 1] = " "
        for i = 1, #recipe.reagents do
            local reg = recipe.reagents[i]
            local enough = (reg.available or 0) >= (reg.count or 0)
            local line = tostring(reg.name or "?") .. " x" .. tostring(reg.count or 0) .. " (" .. tostring(reg.available or 0) .. ")"
            if enough then
                lines[#lines + 1] = "|cff88ff88" .. line .. "|r"
            else
                lines[#lines + 1] = "|cffff8888" .. line .. "|r"
            end
        end
    else
        lines[#lines + 1] = "Aucun composant enregistré."
    end

    self.ui.detailReagents:SetText(table.concat(lines, "\n"))
end

function GMS:RefreshRecipePanel()
    self:RefreshRecipeList()
    self:RefreshRecipeDetail()
end

function GMS:RefreshUI()
    if not self.ui then return end

    self.ui.guildTitle:SetText(getGuildName())
    buildFilteredList()
    buildRecipeList()
    self:RefreshMemberList()
    self:RefreshCenterPanel()
    self:RefreshRecipePanel()
end

SLASH_GUILDEMETIERSSYNC1 = "/gms"
SLASH_GUILDEMETIERSSYNC2 = "/gmetiers"
SlashCmdList["GUILDEMETIERSSYNC"] = function(msgText)
    msgText = tostring(msgText or "")
    msgText = string.gsub(msgText, "^%s+", "")
    msgText = string.gsub(msgText, "%s+$", "")
    msgText = string.gsub(msgText, "%s+", " ")
    msgText = string.lower(msgText)

    if msgText == "sync" then
        GuildRoster()
        updateOwnEntry()
        broadcastSelfAndRecipes("GUILD")
        requestSync(true)
        buildFilteredList()
        buildRecipeList()
        GMS:RefreshUI()
    elseif msgText == "minimap" then
        ensureDB()
        local hidden = GuildeMetiersSyncDB.minimap.hide
        setMinimapVisible(hidden)
        if hidden then
            msg("Bouton minimap affiché.")
        else
            msg("Bouton minimap masqué.")
        end
    elseif msgText == "help" then
        msg("/gms : ouvre la fenêtre")
        msg("/gms sync : force une synchro")
        msg("/gms minimap : affiche/masque le bouton minimap")
    else
        toggleWindow()
    end
end

GMS:SetScript("OnEvent", function(self, event, ...)
    if event == "PLAYER_LOGIN" then
        ensureDB()

        if C_ChatInfo and C_ChatInfo.RegisterAddonMessagePrefix then
            C_ChatInfo.RegisterAddonMessagePrefix(PREFIX)
        elseif RegisterAddonMessagePrefix then
            RegisterAddonMessagePrefix(PREFIX)
        end

        restoreUIState()
        self:CreateUI()
        createMinimapButton()

        GuildRoster()
        updateOwnEntry()
        cleanupStaleData()

        buildFilteredList()
        if not self.selectedPlayer and self.filtered[1] then
            selectPlayer(self.filtered[1].name)
        end
        buildRecipeList()
        self:RefreshUI()

    elseif event == "GUILD_ROSTER_UPDATE" then
        updateRosterFlags()
        buildFilteredList()
        buildRecipeList()
        if self.ui and self.ui.frame and self.ui.frame:IsShown() then
            self:RefreshUI()
        end

    elseif event == "PLAYER_GUILD_UPDATE" then
        GuildRoster()
        updateOwnEntry()
        buildFilteredList()
        buildRecipeList()
        if self.ui and self.ui.frame and self.ui.frame:IsShown() then
            self:RefreshUI()
        end

    elseif event == "CHAT_MSG_ADDON" then
        local prefix, text, channel, sender = ...
        if prefix ~= PREFIX then return end

        sender = trimRealm(sender)
        if sender == UnitName("player") then return end

        local parts = decodeParts(text)
        if parts[1] == "REQ" then
            if channel == "WHISPER" then
                broadcastSelfAndRecipes("WHISPER", sender)
            else
                broadcastSelfAndRecipes("GUILD")
            end
        elseif parts[1] == "DATA" then
            importEntry(parts, sender)
            buildFilteredList()
            if self.ui and self.ui.frame and self.ui.frame:IsShown() then
                self:RefreshUI()
            end
        elseif parts[1] == "RECIPES" then
            importRecipes(parts, sender)
            buildRecipeList()
            if self.ui and self.ui.frame and self.ui.frame:IsShown() then
                self:RefreshUI()
            end
        end

    elseif event == "PLAYER_ENTERING_WORLD" then
        updateOwnEntry()
        buildFilteredList()
        buildRecipeList()
        if self.ui and self.ui.frame and self.ui.frame:IsShown() then
            self:RefreshUI()
        end

    elseif event == "TRADE_SKILL_SHOW" or event == "TRADE_SKILL_UPDATE" then
        scanCurrentTradeSkill()
    end
end)

GMS:RegisterEvent("PLAYER_LOGIN")
GMS:RegisterEvent("PLAYER_ENTERING_WORLD")
GMS:RegisterEvent("PLAYER_GUILD_UPDATE")
GMS:RegisterEvent("GUILD_ROSTER_UPDATE")
GMS:RegisterEvent("CHAT_MSG_ADDON")
GMS:RegisterEvent("TRADE_SKILL_SHOW")
GMS:RegisterEvent("TRADE_SKILL_UPDATE")

GMS:SetScript("OnUpdate", function(self, elapsed)
    self._tick = (self._tick or 0) + elapsed
    if self._tick < 15 then return end
    self._tick = 0

    if IsInGuild() then
        GuildRoster()
    end
end)