-- =====================================================================
-- LiveStrat — Core.lua
-- Init, base de données, commandes, événements de groupe.
-- Modèle de données : le TABLEAU est une liste ordonnée d'OPÉRATIONS
-- vectorielles { id, auteur, type, données } — jamais de pixels. Chaque
-- client rejoue les opérations pour rendre le tableau ; la sync ne
-- transporte que ces opérations (voir Sync.lua).
-- =====================================================================
local ADDON, ns = ...
local L = ns.L

ns.modules = {}

local Core = CreateFrame("Frame")
ns.Core = Core

-- ---------------------------------------------------------------------
-- État
-- ---------------------------------------------------------------------
ns.board = {
    ops = {},          -- liste ordonnée d'opérations (voir Board.lua)
    byId = {},         -- [id] = op (pour suppression/dédoublonnage)
    seq = 0,           -- séquence locale (ids d'opérations)
}
ns.acl = {}            -- [nomComplet] = true — joueurs autorisés par le chef
ns.me = nil            -- "Nom-Royaume" du joueur local

function ns.FullName(unit)
    local n, r = UnitFullName(unit or "player")
    if not n then return nil end
    if not r or r == "" then r = GetNormalizedRealmName() or "" end
    return (r ~= "") and (n .. "-" .. r) or n
end

-- Le CHEF a tous les droits ; les autres selon la liste diffusée.
function ns.CanDraw(who)
    who = who or ns.me
    -- Mode SIMULATION (/tac simu) : les faux membres passent par la
    -- VRAIE liste de droits (vécu : un passe-droit codé en dur rendait
    -- la révocation par clic sans effet — révoquer doit révoquer).
    if ns.simu and who ~= ns.me and not IsInGroup() then
        return ns.acl[who] == true
    end
    if not IsInGroup() then return who == ns.me end   -- solo : entraînement
    if UnitIsGroupLeader("player") and who == ns.me then return true end
    -- qui est le chef actuellement ?
    local leader = ns.GroupLeaderName()
    if who == leader then return true end
    return ns.acl[who] == true
end

function ns.GroupLeaderName()
    if not IsInGroup() then return ns.me end
    local prefix = IsInRaid() and "raid" or "party"
    local n = GetNumGroupMembers()
    for i = 1, n do
        local unit = prefix .. i
        if UnitExists(unit) and UnitIsGroupLeader(unit) then
            return ns.FullName(unit)
        end
    end
    if UnitIsGroupLeader("player") then return ns.me end
    return nil
end

-- ---------------------------------------------------------------------
-- Événements
-- ---------------------------------------------------------------------
Core:RegisterEvent("PLAYER_LOGIN")
Core:RegisterEvent("GROUP_ROSTER_UPDATE")
Core:RegisterEvent("ENCOUNTER_START")
Core:SetScript("OnEvent", function(self, event, ...)
    if event == "PLAYER_LOGIN" then
        LiveStratDB = LiveStratDB or {}
        -- MIGRATION depuis l'ère « Tactica » : plans et réglages repris
        -- une seule fois (l'ancienne base reste listée dans le toc).
        if TacticaDB and not LiveStratDB._migrated then
            for k, v in pairs(TacticaDB) do
                if LiveStratDB[k] == nil then LiveStratDB[k] = v end
            end
            LiveStratDB._migrated = true
        end
        LiveStratDB.boards = LiveStratDB.boards or {}   -- [cléRencontre] = ops sérialisées
        LiveStratDB.minimap = LiveStratDB.minimap or { angle = 210 }
        ns.me = ns.FullName("player")
        if ns.modules.Sync then ns.modules.Sync:Initialize() end
        ns.CreateMinimapButton()
        print("|cff33ccffLiveStrat|r — /livestrat ou /tac pour ouvrir le tableau.")
        -- Guide de PREMIER lancement (une seule fois).
        if not LiveStratDB.seenIntro then
            LiveStratDB.seenIntro = true
            print("|cff33ccffLiveStrat|r — " .. L["Le chef dessine, tout le groupe voit en direct. Donne des droits en cliquant les noms de la colonne de gauche."])
            print("|cff33ccffLiveStrat|r — " .. L["Astuces : clic droit = supprimer · Ctrl+Z = annuler · Maj-glisser = déplacer la vue · molette = zoom · >< = mode combat."])
        end
    elseif event == "GROUP_ROSTER_UPDATE" then
        -- Changement de chef → la liste de droits repart de zéro (le
        -- nouveau chef rediffusera la sienne).
        local B = ns.modules.Board
        if B and B:IsShown() then
            B:UpdatePermissionUI()
            -- GROUPE FORMÉ APRÈS l'ouverture des tableaux (vécu : «
            -- aucun message n'est parti ») : recensement + demande du
            -- tableau, débouncés (le roster part en rafales).
            if IsInGroup() and not self._rosterSync then
                self._rosterSync = true
                C_Timer.After(2, function()
                    self._rosterSync = nil
                    if B:IsShown() and IsInGroup() then
                        ns.modules.Sync:Census()
                        ns.modules.Sync:SendViewing(true)
                        if not UnitIsGroupLeader("player") then
                            ns.modules.Sync:RequestBoard()
                        end
                    end
                end)
            end
        end
    elseif event == "ENCOUNTER_START" then
        -- Pull : si le tableau est ouvert, il bascule sur le boss engagé.
        local encounterID = ...
        local B = ns.modules.Board
        if B and B:IsShown() then B:AutoDetect(encounterID) end
    end
end)

-- ---------------------------------------------------------------------
-- Pastille MINIMAP (maison, zéro lib) : glissable autour du cadran,
-- angle mémorisé, clic = tableau.
-- ---------------------------------------------------------------------
function ns.CreateMinimapButton()
    local b = CreateFrame("Button", "LiveStratMinimapButton", Minimap)
    b:SetSize(31, 31)
    b:SetFrameLevel(Minimap:GetFrameLevel() + 8)
    b:RegisterForClicks("LeftButtonUp")
    b:RegisterForDrag("LeftButton")

    b.icon = b:CreateTexture(nil, "BACKGROUND")
    b.icon:SetSize(19, 19)
    b.icon:SetPoint("CENTER", -1, 1)
    b.icon:SetTexture("Interface\\AddOns\\LiveStrat\\Portrait.png")
    local mask = b:CreateMaskTexture()
    mask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask",
        "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
    mask:SetAllPoints(b.icon)
    b.icon:AddMaskTexture(mask)

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

    local function place()
        local a = math.rad(LiveStratDB.minimap.angle or 210)
        b:ClearAllPoints()
        b:SetPoint("CENTER", Minimap, "CENTER",
            math.cos(a) * 80, math.sin(a) * 80)
    end
    place()

    b:SetScript("OnDragStart", function()
        b:SetScript("OnUpdate", function()
            local mx, my = Minimap:GetCenter()
            local cx, cy = GetCursorPosition()
            local s = Minimap:GetEffectiveScale()
            LiveStratDB.minimap.angle = math.deg(
                math.atan2(cy / s - my, cx / s - mx))
            place()
        end)
    end)
    b:SetScript("OnDragStop", function() b:SetScript("OnUpdate", nil) end)
    b:SetScript("OnClick", function()
        if ns.modules.Board then ns.modules.Board:Toggle() end
    end)
    b:SetScript("OnEnter", function()
        GameTooltip:SetOwner(b, "ANCHOR_LEFT")
        GameTooltip:SetText("LiveStrat")
        GameTooltip:AddLine(ns.L["Tableau tactique"], 1, 1, 1)
        GameTooltip:Show()
    end)
    b:SetScript("OnLeave", function() GameTooltip:Hide() end)
end

-- ---------------------------------------------------------------------
-- Commandes
-- ---------------------------------------------------------------------
-- ---------------------------------------------------------------------
-- SIMULATION (/tac simu) : un faux raid joué en local — les messages
-- passent par Sync:OnMessage comme s'ils venaient du réseau. Teste sans
-- 2e compte : colonne En direct, réception de dessins, droits (dont un
-- INTRUS non autorisé qui doit être ignoré), mise à jour de position.
-- ---------------------------------------------------------------------
local SIMU_FAKES = {
    { name = "Kayatank-Simulé", short = "Kayatank", class = "WARRIOR" },
    { name = "Lumissa-Simulé", short = "Lumissa", class = "PRIEST" },
    { name = "Zorgrim-Simulé", short = "Zorgrim", class = "MAGE" },
    { name = "Sanspatch-Simulé", short = "Sanspatch", class = "ROGUE" },
}

function ns.StartSimu()
    local Sync = ns.modules.Sync
    local Board = ns.modules.Board
    if not (Sync and Board) then return end
    ns.simu = true
    ns.simuMembers = SIMU_FAKES
    -- Kayatank est AUTORISÉE (vraie liste ns.acl — un clic sur son nom
    -- la révoque POUR DE VRAI) ; Zorgrim ne l'est PAS (test sécurité).
    ns.acl["Kayatank-Simulé"] = true
    print("|cff33ccffLiveStrat|r simulation : faux raid actif — ouvre le tableau et observe.")
    local function rx(msg, from) return function() Sync:OnMessage(msg, from) end end
    -- Recensement : deux répondent (dont une qui regarde), Sanspatch jamais.
    C_Timer.After(0.8, rx("HI\0011", "Kayatank-Simulé"))
    C_Timer.After(1.2, rx("HI\0010", "Lumissa-Simulé"))
    C_Timer.After(1.6, rx("HI\0010", "Zorgrim-Simulé"))
    C_Timer.After(3.0, rx("VIS\0011", "Zorgrim-Simulé"))
    -- Kayatank (autorisée) dessine : un cercle puis un marqueur tank…
    C_Timer.After(4.0, rx("OP\001C:1:2:420,380;500,380", "Kayatank-Simulé"))
    C_Timer.After(5.5, rx("OP\001M:2:1:420,380:Kayatank", "Kayatank-Simulé"))
    -- …qu'elle DÉPLACE (même id = mise à jour, pas un doublon).
    C_Timer.After(7.0, rx("OP\001M:2:1:520,300:Kayatank", "Kayatank-Simulé"))
    C_Timer.After(8.5, rx("OP\001M:2:1:600,420:Kayatank", "Kayatank-Simulé"))
    -- L'INTRUS : Zorgrim n'a pas les droits — son trait doit être IGNORÉ.
    C_Timer.After(9.5, rx("OP\001N:1:1:100,100;900,900", "Zorgrim-Simulé"))
    C_Timer.After(10.5, function()
        print("|cff33ccffLiveStrat|r simulation : si aucun GRAND TRAIT diagonal n'est apparu, la sécurité fonctionne (l'intrus a été ignoré).")
    end)
    -- Lumissa ouvre son tableau plus tard.
    C_Timer.After(12, rx("VIS\0011", "Lumissa-Simulé"))
    if Board.UpdatePermissionUI then Board:UpdatePermissionUI() end
end

function ns.StopSimu()
    ns.simu = nil
    ns.simuMembers = nil
    for _, m in ipairs(SIMU_FAKES) do
        ns.acl[m.name] = nil
        ns.present[m.name] = nil
        ns.viewing[m.name] = nil
    end
    local Board = ns.modules.Board
    if Board and Board.UpdatePermissionUI then Board:UpdatePermissionUI() end
    print("|cff33ccffLiveStrat|r simulation coupée (les dessins simulés restent — clic droit ou Tout effacer).")
end

-- Invitation reçue (OPN) : une ligne de chat avec un LIEN CLIQUABLE qui
-- ouvre le tableau — affichée LOCALEMENT par l'addon du destinataire
-- (le serveur interdit d'envoyer des liens personnalisés, mais une
-- ligne locale peut tout contenir ; seuls les porteurs de l'addon
-- peuvent ouvrir le tableau, donc seuls eux ont besoin du lien).
function ns.ShowOpenInvitePopup(from)
    local short = (from or "?"):match("^([^%-]+)") or from
    local link = "|cff40ff40|Haddon:LiveStrat:open|h[" .. ns.L["Ouvrir le tableau"] .. "]|h|r"
    print(("|cff33ccffLiveStrat|r — %s %s %s")
        :format(short, ns.L["partage un plan tactique :"], link))
    PlaySound(SOUNDKIT.RAID_WARNING or 8959)
end

-- Le clic sur le lien passe par SetItemRef (dispatch officiel des liens).
hooksecurefunc("SetItemRef", function(link)
    if type(link) == "string" and link:find("^addon:LiveStrat:open") then
        if ns.modules.Board and not ns.modules.Board:IsShown() then
            ns.modules.Board:Toggle()
        end
    end
end)

SLASH_LIVESTRAT1 = "/livestrat"
SLASH_LIVESTRAT2 = "/lstrat"
SLASH_LIVESTRAT3 = "/tac"   -- alias historique
SlashCmdList.LIVESTRAT = function(msg)
    msg = (msg or ""):lower():gsub("^%s+", ""):gsub("%s+$", "")
    if msg == "debug" then
        ns.debug = not ns.debug
        print("LiveStrat debug :", ns.debug and "ON" or "OFF")
        return
    end
    if msg == "simu" then
        if ns.simu then ns.StopSimu() else ns.StartSimu() end
        return
    end
    if ns.modules.Board then ns.modules.Board:Toggle() end
end

function ns.Log(...)
    if not ns.debug then return end
    print("|cff33ccff[tac]|r", ...)
end

-- ---------------------------------------------------------------------
-- BOUTON MODERNE maison : plat sombre, liseré fin, survol doré, état
-- actif bleu — zéro template/atlas Blizzard (le rouge standard jurait,
-- retour utilisateur), donc un rendu identique partout, pour toujours.
-- ---------------------------------------------------------------------
local BTN = {
    bg       = { 0.115, 0.125, 0.155, 0.95 },
    bgHover  = { 0.165, 0.180, 0.220, 1 },
    border   = { 0.30, 0.33, 0.40, 1 },
    hover    = { 0.95, 0.78, 0.25, 1 },      -- liseré doré au survol
    activeBg = { 0.10, 0.32, 0.55, 1 },      -- bleu « sélectionné »
    activeBd = { 0.35, 0.70, 1.00, 1 },
}

function ns.CreateModernButton(parent, w, h, label)
    local b = CreateFrame("Button", nil, parent, "BackdropTemplate")
    b:SetSize(w, h or 24)
    b:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 })
    b.text = b:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    b.text:SetPoint("CENTER", 0, 0)
    if label then b.text:SetText(label) end

    function b:Repaint()
        local hov = self._hover
        if self._active then
            self:SetBackdropColor(unpack(BTN.activeBg))
            self:SetBackdropBorderColor(unpack(hov and BTN.hover or BTN.activeBd))
        else
            self:SetBackdropColor(unpack(hov and BTN.bgHover or BTN.bg))
            self:SetBackdropBorderColor(unpack(hov and BTN.hover or BTN.border))
        end
    end

    function b:SetActive(on)
        self._active = on and true or false
        self:Repaint()
    end

    b:SetScript("OnEnter", function(s) s._hover = true s:Repaint() end)
    b:SetScript("OnLeave", function(s) s._hover = nil s:Repaint() end)
    b:SetScript("OnMouseDown", function(s) s.text:SetPoint("CENTER", 0, -1) end)
    b:SetScript("OnMouseUp", function(s) s.text:SetPoint("CENTER", 0, 0) end)
    b:Repaint()
    return b
end
