local _, addon = ...
local HLY = addon.HLY

local WHITE_TEXTURE = "Interface\\Buttons\\WHITE8X8"

local C_LFGList = C_LFGList
local C_Timer = C_Timer
local time = time
local pairs = pairs
local ipairs = ipairs
local tonumber = tonumber
local type = type
local UnitFullName = UnitFullName
local UnitExists = UnitExists
local UnitIsUnit = UnitIsUnit
local IsInRaid = IsInRaid
local GetNumGroupMembers = GetNumGroupMembers
local GetNormalizedRealmName = GetNormalizedRealmName
local GetRaidRosterInfo = GetRaidRosterInfo
local wipe = wipe

local pendingLeaders = {}
local pendingGroupMembers = {}

local function SafeCall(func, ...)
    if not func then
        return nil
    end
    local ok, result = pcall(func, ...)
    if ok then
        return result
    end
    return nil
end

local function IsEnabled()
    return HLY.db and HLY.db.notAgain and HLY.db.notAgain.enabled
end

local function IsPlayersEnabled()
    return HLY.db and HLY.db.notAgain and HLY.db.notAgain.playersEnabled
end

local function GetDurationSeconds()
    local db = HLY.db and HLY.db.notAgain
    local minutes = db and tonumber(db.duration) or 60
    if minutes < 1 then
        minutes = 1
    end
    return minutes * 60
end

local function GetPlayersDurationSeconds()
    local db = HLY.db and HLY.db.notAgain
    local minutes = db and tonumber(db.playersDuration) or 60
    if minutes < 1 then
        minutes = 1
    end
    return minutes * 60
end

local function GetSeenStore()
    local db = HLY.db and HLY.db.notAgain
    if not db then
        return nil
    end
    db.seen = db.seen or {}
    return db.seen
end

local function GetSeenPlayersStore()
    local db = HLY.db and HLY.db.notAgain
    if not db then
        return nil
    end
    db.seenPlayers = db.seenPlayers or {}
    return db.seenPlayers
end

local function NormalizePlayer(name)
    if type(name) ~= "string" or name == "" then
        return nil
    end
    name = name:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "")

    local base, realm = name:match("^(.-)%-(.+)$")
    if not base then
        base = name
        realm = GetNormalizedRealmName and GetNormalizedRealmName() or ""
    end
    if base == "" then
        return nil
    end

    return (base .. "-" .. (realm or "")):lower():gsub("[%s'%._]", "")
end

local function NormalizeLeader(name)
    if type(name) ~= "string" or name == "" then
        return nil
    end
    name = name:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "")
    if name == "" then
        return nil
    end
    return name:lower()
end

local function PruneStore(store, cutoff)
    if not store then
        return
    end
    for key, ts in pairs(store) do
        if type(ts) ~= "number" or ts < cutoff then
            store[key] = nil
        end
    end
end

local function PruneSeen()
    PruneStore(GetSeenStore(), time() - GetDurationSeconds())
    PruneStore(GetSeenPlayersStore(), time() - GetPlayersDurationSeconds())
end

local function RecordLeader(leaderName)
    local store = GetSeenStore()
    local key = NormalizeLeader(leaderName)
    if not store or not key then
        return
    end
    store[key] = time()
end

local function HasSeenLeader(leaderName)
    local store = GetSeenStore()
    local key = NormalizeLeader(leaderName)
    if not store or not key then
        return false
    end
    local ts = store[key]
    if type(ts) ~= "number" then
        return false
    end
    if (time() - ts) > GetDurationSeconds() then
        store[key] = nil
        return false
    end
    return true
end

local function RecordPlayer(name)
    local store = GetSeenPlayersStore()
    local key = NormalizePlayer(name)
    if not store or not key then
        return
    end
    store[key] = time()
end

local function HasSeenPlayer(name)
    local store = GetSeenPlayersStore()
    local key = NormalizePlayer(name)
    if not store or not key then
        return false
    end
    local ts = store[key]
    if type(ts) ~= "number" then
        return false
    end
    if (time() - ts) > GetPlayersDurationSeconds() then
        store[key] = nil
        return false
    end
    return true
end

local function GetSearchResultLeader(searchResultID)
    if not searchResultID or not C_LFGList or not C_LFGList.GetSearchResultInfo then
        return nil
    end
    local info = SafeCall(C_LFGList.GetSearchResultInfo, searchResultID)
    if type(info) ~= "table" then
        return nil
    end
    return info.leaderName
end

local function IsGroupFinderLoaded()
    if C_AddOns and C_AddOns.IsAddOnLoaded then
        return C_AddOns.IsAddOnLoaded("Blizzard_GroupFinder")
    end
    if IsAddOnLoaded then
        return IsAddOnLoaded("Blizzard_GroupFinder")
    end
    return LFGListFrame ~= nil
end

local function GetBarColor()
    local db = HLY.db and HLY.db.notAgain
    local c = db and db.barColor
    if type(c) == "table" then
        return c.r or 0.95, c.g or 0.55, c.b or 0.1
    end
    return 0.95, 0.55, 0.1
end

local function GetPlayersBarColor()
    local db = HLY.db and HLY.db.notAgain
    local c = db and db.playersBarColor
    if type(c) == "table" then
        return c.r or 0.25, c.g or 0.6, c.b or 0.95
    end
    return 0.25, 0.6, 0.95
end

local function UpdateSearchEntryBar(entry)
    if not entry then
        return
    end

    local bar = entry.__HLYNotAgainBar
    if not bar then
        bar = entry:CreateTexture(nil, "OVERLAY")
        bar:SetTexture(WHITE_TEXTURE)
        entry.__HLYNotAgainBar = bar
    end

    local r, g, b = GetBarColor()
    bar:SetColorTexture(r, g, b, 0.95)
    bar:ClearAllPoints()
    bar:SetPoint("TOPLEFT", entry, "TOPLEFT", 2, -1)
    bar:SetPoint("BOTTOMLEFT", entry, "BOTTOMLEFT", 2, -2)
    bar:SetWidth(4)

    local leader = entry.resultID and GetSearchResultLeader(entry.resultID)
    if IsEnabled() and leader and HasSeenLeader(leader) then
        bar:Show()
    else
        bar:Hide()
    end
end

local function UpdateApplicantMemberBar(member, fullName)
    if not member then
        return
    end

    local bar = member.__HLYNotAgainBar
    if not bar then
        bar = member:CreateTexture(nil, "OVERLAY")
        bar:SetTexture(WHITE_TEXTURE)
        member.__HLYNotAgainBar = bar
    end

    local r, g, b = GetPlayersBarColor()
    bar:SetColorTexture(r, g, b, 0.95)
    bar:ClearAllPoints()
    bar:SetPoint("TOPLEFT", member, "TOPLEFT", 0, 1)
    bar:SetPoint("BOTTOMLEFT", member, "BOTTOMLEFT", 0, -1)
    bar:SetWidth(4)

    if IsPlayersEnabled() and fullName and HasSeenPlayer(fullName) then
        bar:Show()
    else
        bar:Hide()
    end
end

-- Track current group members in a pending set. As soon as a member drops out
-- of the roster (left/kicked, or you left the whole group) they are written to
-- the persistent store, so they get marked while members still with you do not.
function HLY:TrackNotAgainGroupMembers()
    if not IsPlayersEnabled() then
        return
    end

    local total = GetNumGroupMembers() or 0
    local current = {}

    if total > 0 then
        local inRaid = IsInRaid()
        local prefix = inRaid and "raid" or "party"
        local count = inRaid and total or (total - 1)

        for i = 1, count do
            local unit = prefix .. i
            if UnitExists(unit) and not UnitIsUnit(unit, "player") then
                local name, realm = UnitFullName(unit)
                if name then
                    local key = NormalizePlayer(realm and realm ~= "" and (name .. "-" .. realm) or name)
                    if key then
                        current[key] = true
                    end
                end
            end
        end
    end

    local store = GetSeenPlayersStore()
    local now = time()
    local flushed = false

    for key in pairs(pendingGroupMembers) do
        if not current[key] then
            if store then
                store[key] = now
            end
            pendingGroupMembers[key] = nil
            flushed = true
        end
    end

    for key in pairs(current) do
        pendingGroupMembers[key] = true
    end

    if flushed then
        self:RefreshNotAgain()
    end
end

local function UpdateRaidGroupButton(button)
    if not button then
        return
    end

    local bar = button.__HLYNotAgainBar
    if not bar then
        bar = button:CreateTexture(nil, "OVERLAY")
        bar:SetTexture(WHITE_TEXTURE)
        button.__HLYNotAgainBar = bar
    end

    local r, g, b = GetPlayersBarColor()
    bar:SetColorTexture(r, g, b, 0.95)
    bar:ClearAllPoints()
    bar:SetPoint("TOPLEFT", button, "TOPLEFT", 0, 0)
    bar:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", 0, 0)
    bar:SetWidth(4)

    local id = button.GetID and button:GetID()
    local name
    if id and id > 0 and GetRaidRosterInfo then
        name = SafeCall(GetRaidRosterInfo, id)
    end

    if IsPlayersEnabled() and name and HasSeenPlayer(name) then
        bar:Show()
    else
        bar:Hide()
    end
end

function HLY:UpdateRaidFrameMarks()
    local maxMembers = MAX_RAID_MEMBERS or 40
    for i = 1, maxMembers do
        local button = _G["RaidGroupButton" .. i]
        if button then
            UpdateRaidGroupButton(button)
        end
    end
end

function HLY:RefreshNotAgain()
    if IsGroupFinderLoaded() and LFGListFrame then
        if LFGListFrame.SearchPanel and LFGListFrame.SearchPanel.ScrollBox then
            local scrollTarget = LFGListFrame.SearchPanel.ScrollBox.ScrollTarget
            if scrollTarget and scrollTarget.GetChildren then
                for _, child in ipairs({ scrollTarget:GetChildren() }) do
                    if child and child.resultID and SafeCall(C_LFGList.HasSearchResultInfo, child.resultID) then
                        UpdateSearchEntryBar(child)
                    end
                end
            end
        end

        if LFGListFrame.ApplicationViewer and LFGListFrame.ApplicationViewer.ScrollBox then
            local scrollTarget = LFGListFrame.ApplicationViewer.ScrollBox.ScrollTarget
            if scrollTarget and scrollTarget.GetChildren then
                for _, child in ipairs({ scrollTarget:GetChildren() }) do
                    if child and child.applicantID and child.Members then
                        for _, member in ipairs(child.Members) do
                            if member and member.memberIdx then
                                local fullName = SafeCall(C_LFGList.GetApplicantMemberInfo, child.applicantID, member.memberIdx)
                                UpdateApplicantMemberBar(member, fullName)
                            end
                        end
                    end
                end
            end
        end
    end

    self:UpdateRaidFrameMarks()
end

function HLY:HandleNotAgainStatusUpdate(searchResultID, newStatus)
    if not searchResultID then
        return
    end

    local leader = GetSearchResultLeader(searchResultID)
    if leader then
        pendingLeaders[searchResultID] = leader
    end

    if type(newStatus) == "string" and newStatus:lower() == "inviteaccepted" then
        RecordLeader(leader or pendingLeaders[searchResultID])
        pendingLeaders[searchResultID] = nil
        self:RefreshNotAgain()
    end
end

function HLY:HandleNotAgainJoinedGroup(searchResultID)
    if not searchResultID then
        return
    end
    RecordLeader(GetSearchResultLeader(searchResultID) or pendingLeaders[searchResultID])
    pendingLeaders[searchResultID] = nil
    self:RefreshNotAgain()
end

function HLY:ResetNotAgainHistory()
    local db = self.db and self.db.notAgain
    if db then
        db.seen = {}
        db.seenPlayers = {}
    end
    self:RefreshNotAgain()
end

function HLY:InstallNotAgainHooks()
    if self.notAgainHooksInstalled or not IsGroupFinderLoaded() then
        return
    end

    hooksecurefunc("LFGListSearchEntry_Update", function(entry)
        if not entry or not entry.resultID or not SafeCall(C_LFGList.HasSearchResultInfo, entry.resultID) then
            return
        end
        UpdateSearchEntryBar(entry)
    end)

    hooksecurefunc("LFGListApplicationViewer_UpdateApplicantMember", function(member, applicantID, memberIdx)
        local fullName = SafeCall(C_LFGList.GetApplicantMemberInfo, applicantID, memberIdx)
        UpdateApplicantMemberBar(member, fullName)
    end)

    self.notAgainHooksInstalled = true
    self:RefreshNotAgain()
end

function HLY:InstallNotAgainRaidHook()
    if self.notAgainRaidHooked then
        return
    end
    if type(_G.RaidGroupFrame_Update) ~= "function" then
        return
    end

    hooksecurefunc("RaidGroupFrame_Update", function()
        self:UpdateRaidFrameMarks()
    end)

    self.notAgainRaidHooked = true
    self:UpdateRaidFrameMarks()
end

function HLY:HandleNotAgainEvent(event, ...)
    if event == "LFG_LIST_APPLICATION_STATUS_UPDATED" then
        self:HandleNotAgainStatusUpdate(...)
    elseif event == "LFG_LIST_JOINED_GROUP" then
        self:HandleNotAgainJoinedGroup(...)
    elseif event == "GROUP_ROSTER_UPDATE" then
        self:TrackNotAgainGroupMembers()
    elseif event == "ADDON_LOADED" then
        local loadedAddon = ...
        if loadedAddon == "Blizzard_GroupFinder" then
            self:InstallNotAgainHooks()
        elseif loadedAddon == "Blizzard_RaidUI" then
            self:InstallNotAgainRaidHook()
        end
    end
end

function HLY:SetupNotAgain()
    if self.notAgainSetup then
        self:InstallNotAgainHooks()
        self:InstallNotAgainRaidHook()
        return
    end

    PruneSeen()

    self.notAgainEventFrame = CreateFrame("Frame")
    self.notAgainEventFrame:RegisterEvent("LFG_LIST_APPLICATION_STATUS_UPDATED")
    self.notAgainEventFrame:RegisterEvent("LFG_LIST_JOINED_GROUP")
    self.notAgainEventFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
    self.notAgainEventFrame:RegisterEvent("ADDON_LOADED")
    self.notAgainEventFrame:SetScript("OnEvent", function(_, event, ...)
        self:HandleNotAgainEvent(event, ...)
    end)

    self:TrackNotAgainGroupMembers()
    self:InstallNotAgainHooks()
    self:InstallNotAgainRaidHook()
    self.notAgainSetup = true
end

-- LibHLYUI options pane (v2) ------------------------------------------------
function HLY:BuildNotAgainOptions(tab)
    tab:TabGroup({
        {
            name = "Gruppen",
            build = function(p)
                p:Note("Merkt sich den Anführer jeder Gruppe, der du beigetreten bist, und markiert diese Gruppen in der Suchliste des Gruppenfinders. Einträge verfallen nach der Merkdauer.")
                p:Checkbox({
                    label = "Gruppen markieren",
                    get = function() return self.db.notAgain.enabled end,
                    set = function(v) self.db.notAgain.enabled = v self:RefreshNotAgain() end,
                })
                p:Slider({
                    label = "Merkdauer (Minuten)", min = 5, max = 240, step = 5,
                    get = function() return self.db.notAgain.duration end,
                    set = function(v) self.db.notAgain.duration = v self:RefreshNotAgain() end,
                })
                p:Color({
                    label = "Balkenfarbe",
                    get = function() local c = self.db.notAgain.barColor return c.r, c.g, c.b end,
                    set = function(r, g, b) local c = self.db.notAgain.barColor c.r, c.g, c.b = r, g, b self:RefreshNotAgain() end,
                })
                p:Button({ label = "Verlauf löschen", func = function() self:ResetNotAgainHistory() end })
            end,
        },
        {
            name = "Mitspieler",
            build = function(p)
                p:Note("Merkt sich Spieler, mit denen du in einer Gruppe warst (gespeichert beim Verlassen), und markiert sie in der Bewerberliste sowie im Raid-Tab des Freunde-Fensters.")
                p:Checkbox({
                    label = "Mitspieler markieren",
                    get = function() return self.db.notAgain.playersEnabled end,
                    set = function(v)
                        self.db.notAgain.playersEnabled = v
                        if v then self:TrackNotAgainGroupMembers() end
                        self:RefreshNotAgain()
                    end,
                })
                p:Slider({
                    label = "Merkdauer (Minuten)", min = 5, max = 240, step = 5,
                    get = function() return self.db.notAgain.playersDuration end,
                    set = function(v) self.db.notAgain.playersDuration = v self:RefreshNotAgain() end,
                })
                p:Color({
                    label = "Balkenfarbe",
                    get = function() local c = self.db.notAgain.playersBarColor return c.r, c.g, c.b end,
                    set = function(r, g, b) local c = self.db.notAgain.playersBarColor c.r, c.g, c.b = r, g, b self:RefreshNotAgain() end,
                })
                p:Button({ label = "Verlauf löschen", func = function() self:ResetNotAgainHistory() end })
            end,
        },
    })
end

addon.options = addon.options or {}
addon.options[#addon.options + 1] = {
    group = "Group Finder", groupOrder = 250,
    name = "Déjà Vu", order = 2, ownTabs = true,
    build = "BuildNotAgainOptions",
}
