-- TOG Tools — Guild Log sub-tab (UI)
--
-- Renders inside the Logs outer tab's nested AceGUI TabGroup. Layout:
--   Filter row: Guild ▾   Date range ▾   [🔍 search]
--   Data table: Time · Guild · Type · Target · Rank · Actor · Detail
--
-- Type filtering now lives in the column-header menu and the single
-- search box (case-insensitive substring across every column). The Guild
-- dropdown is NOT a display filter — it selects WHICH per-guild bucket of
-- entries to load — so it stays as a dropdown even though Guild is also a
-- column.
--
-- Per-session filter state lives on the module table.

local _, addon = ...
local AceGUI = LibStub("AceGUI-3.0")

local GuildLog = addon.logCategories["guild"]

GuildLog._filterGuild        = GuildLog._filterGuild        or nil
GuildLog._filterRange        = GuildLog._filterRange        or "all"
GuildLog._filterCustomFrom   = GuildLog._filterCustomFrom   or nil
GuildLog._filterCustomTo     = GuildLog._filterCustomTo     or nil
GuildLog._filterCalendarOpen = GuildLog._filterCalendarOpen or false
GuildLog._search             = GuildLog._search             or ""

-- ---------------------------------------------------------------------------
-- Event-type metadata
-- ---------------------------------------------------------------------------
local TYPE_LABEL = {
    join    = "Joined",
    leave   = "Left",
    quit    = "Left",       -- treated as leave for display
    remove  = "Kicked",
    promote = "Promoted",
    demote  = "Demoted",
    invite  = "Invited",
}

local TYPE_COLOR = {
    join    = "ff66ff66",
    leave   = "ffffcc66",
    quit    = "ffffcc66",
    remove  = "ffff5555",
    promote = "ff66ccff",
    demote  = "ffffaa55",
    invite  = "ffaaaaaa",
}

-- ---------------------------------------------------------------------------
-- Cell formatters
-- ---------------------------------------------------------------------------
-- Shared with GuildBankLogSubTab via Compat.lua. These were byte-identical
-- private copies until 2026-08-17, and the divergence was a live bug rather
-- than a tidiness point: reviewer finding Y (a server-clock ts rendered through
-- `date()`, which formats in the machine's timezone) was raised and fixed
-- against the Guild Bank copy only, because nothing connected the two. Both
-- logs derive `ts` identically -- tupleToTs subtracts Blizzard's relative
-- offsets from GetServerTime() at hour precision -- so both need the same
-- rendering. Local alias so the call sites below read unchanged.
local fmtTime = addon.GUI.FormatServerHourTime

local function fmtType(t)
    local label = TYPE_LABEL[t] or (t and t:sub(1,1):upper() .. t:sub(2)) or "?"
    local color = TYPE_COLOR[t] or "ffffffff"
    return "|c" .. color .. label .. "|r"
end

local function fmtPlayer(name)
    if not name or name == "" then return "|cff888888—|r" end
    return name
end

local function fmtDetail(_)
    -- Detail used to show the rank for promote / demote rows, but the
    -- Rank column now handles that universally. Detail is reserved for
    -- future event-specific annotations (multi-event causes, banner
    -- rich-text, etc.) — return blank for now so the column doesn't
    -- duplicate the Rank cell.
    return ""
end

-- Shared with GuildBankLogSubTab via Compat.lua -- these were byte-identical
-- private copies until 2026-08-16. Kept as a local alias so the call sites below
-- read the same as before.
local fmtGuild = addon.GUI.FormatGuildKey

-- ---------------------------------------------------------------------------
-- (The in-tab "Clear data" button + two-step confirm helper were
-- removed in v0.5.2; clearing is centralised in Settings > Clear Data,
-- which uses AceConfig's native confirm popup.)

-- ---------------------------------------------------------------------------
-- Filter dropdowns
-- ---------------------------------------------------------------------------
-- Was a private copy identical to GuildBankLogSubTab's but for the module it
-- read. It now also gets the tooltip that only the Guild Bank copy had -- the
-- drift the duplication caused, and the reason merging was worth doing.
local function buildGuildDropdown(onChange)
    return addon.GUI.GuildFilterDropdown(GuildLog, onChange, "Guild filter",
        "Which guild's event history to show. Defaults to your currently logged-in character's guild. Switch to |cffffd700All guilds|r to merge entries from every guild your alts belong to into one view; same-realm guilds drop the realm suffix for compactness, cross-realm shows Guild-Realm.")
end

-- ---------------------------------------------------------------------------
-- Draw
-- ---------------------------------------------------------------------------
function GuildLog:Draw(container)
    container:SetLayout("Flow")

    local function rebuild()
        if addon.MainWindow and addon.MainWindow.Refresh then
            addon.MainWindow:Refresh()
        end
    end

    local rl   -- data table; forward-declared so the search box can reach it

    -- ---- Filter row -----------------------------------------------------
    local filterRow = AceGUI:Create("SimpleGroup")
    filterRow:SetFullWidth(true)
    filterRow:SetLayout("Flow")
    container:AddChild(filterRow)

    filterRow:AddChild(buildGuildDropdown(rebuild))
    filterRow:AddChild(addon.DateRangePicker:Create({
        default  = self._filterRange,
        onChange = function(key)
            local enteringCustom = (key == "custom" and self._filterRange ~= "custom")
            self._filterRange = key
            if enteringCustom then self._filterCalendarOpen = true end
            rebuild()
        end,
    }))

    local search = AceGUI:Create("EditBox")
    search:SetWidth(240)
    search:DisableButton(true)
    search:SetText(self._search or "")
    search:SetCallback("OnTextChanged", function(_, _, text)
        self._search = text
        if rl then rl:SetSearch(text) end
    end)
    addon.UI.StyleSearchBox(search, true)
    filterRow:AddChild(search)

    -- (No manual Refresh button — the engine auto-captures on
    -- PLAYER_LOGIN, every GUILD_ROSTER_UPDATE, and a 2-minute ticker.
    -- GUILD_ROSTER_UPDATE covers every event type except pending
    -- invites; the ticker covers those within 2 min.)

    if self._filterRange == "custom" and self._filterCustomFrom
       and not self._filterCalendarOpen then
        local pickBtn = AceGUI:Create("Button")
        pickBtn:SetWidth(140)
        pickBtn:SetText("Pick range...")
        pickBtn:SetCallback("OnClick", function()
            self._filterCalendarOpen = true
            rebuild()
        end)
        filterRow:AddChild(pickBtn)
    end

    -- Resolve the effective guild filter once — used both by the calendar
    -- availability query (so it only shows days with matching entries)
    -- and by the table query below.
    local guildKey = self._filterGuild
    if guildKey == "__all" then guildKey = nil end
    if not guildKey then
        guildKey = GuildLog:GetCurrentGuildKey()
        if self._filterGuild == "__all" then guildKey = nil end
    end

    if self._filterRange == "custom"
       and (not self._filterCustomFrom or self._filterCalendarOpen) then
        local unbounded = GuildLog:GetEntries(guildKey, nil, nil, nil)
        local timestamps = {}
        for _, e in ipairs(unbounded) do tinsert(timestamps, e.ts) end
        container:AddChild(addon.DateRangePicker:CreateCalendar({
            defaultFrom = self._filterCustomFrom,
            defaultTo   = self._filterCustomTo,
            timestamps  = timestamps,
            onApply     = function(fromTs, toTs)
                self._filterCustomFrom   = fromTs
                self._filterCustomTo     = toTs
                self._filterCalendarOpen = false
                rebuild()
            end,
        }))
        if not self._filterCustomFrom then return end
    end

    -- Spacer
    local sp = AceGUI:Create("Label")
    sp:SetFullWidth(true); sp:SetText(" ")
    container:AddChild(sp)

    -- ---- Data table -----------------------------------------------------
    local body = AceGUI:Create("SimpleGroup")
    body:SetFullWidth(true)
    body:SetFullHeight(true)
    body:SetLayout("List")
    container:AddChild(body)

    local fromTs, toTs = addon.DateRangePicker:Resolve(
        self._filterRange, self._filterCustomFrom, self._filterCustomTo)
    local entries = GuildLog:GetEntries(guildKey, nil, fromTs, toTs)

    if #entries == 0 then
        local empty = AceGUI:Create("Label")
        empty:SetFullWidth(true)
        local msg = "|cff888888No guild events match the current filters.|r"
        if not GuildLog:GetCurrentGuildKey() then
            msg = msg .. "\n\nThis character isn't in a guild — Guild Log only captures events for guilds your alts are members of. Log into an alt with guild access to populate the log."
        else
            msg = msg .. "\n\nGuild Log persists events beyond Blizzard's in-game ~100-entry cap. The engine auto-captures on login, on every roster change, and on a 2-minute safety ticker. If a recent event isn't showing up yet, give it a moment, or widen the |cffffd700Date range|r, clear the |cffffd700search|r box, or check the column-header filters."
        end
        empty:SetText(msg)
        body:AddChild(empty)
        return
    end

    -- Guild Log opts into the autoFit column model: every column except
    -- the rightmost (Detail) declares `autoFit = true` + `minWidth`, and
    -- RowList measures the widest cell in the CAPTURED set on each SetData
    -- call, growing col.width to fit. Not the visible set: SetData measures
    -- self.data, and Refresh never re-fits, so filtering or searching does
    -- NOT narrow a column (peer-review finding rev-E -- this comment said
    -- "visible" and was one of 28 places across 7 files making that claim). Detail stays auto-stretchy (no width),
    -- absorbing whatever space remains — so it's the only column that
    -- can be forced to truncate when the window is too narrow. Other
    -- log sub-tabs are unchanged; this is being trialed on Guild Log
    -- only before being lifted into a shared layout pass.
    rl = addon.RowList:New(body.content, {
        rowHeight = 16,
        columns = {
            { key = "ts", header = "Time", autoFit = true, minWidth = 70, justify = "LEFT",
              sortKey = "ts", sortDescDefault = true,
              headerTip = "Time",
              headerTipDesc = "When the event happened. Blizzard's guild-event API only exposes hour precision (no minute/second), so older entries show as MM/DD HH:00. Recent entries show as \"Nh ago\". Click to sort by time. Column auto-sizes to fit the longest captured timestamp.",
              format = function(e) return fmtTime(e.ts) end },

            { key = "guildKey", header = "Guild", autoFit = true, minWidth = 70, justify = "LEFT",
              headerTip = "Guild",
              headerTipDesc = "Which guild this event happened in. Same-realm guilds drop the realm suffix for compactness; cross-realm shows Guild-Realm. The Guild filter dropdown above defaults to your current guild. Column auto-sizes to fit the longest captured guild name.",
              format = function(e) return fmtGuild(e.guildKey) end },

            { key = "type", header = "Type", autoFit = true, minWidth = 60, justify = "LEFT",
              filterable = true,
              headerTip = "Event type",
              headerTipDesc = "What happened. Joined / Left / Kicked / Promoted / Demoted / Invited / Quit. Colour-coded: green = join, yellow = leave/quit, red = kick, blue = promote, orange = demote, grey = invite. Column auto-sizes to the widest event-type label.",
              format = function(e) return fmtType(e.type) end },

            { key = "target", header = "Player", autoFit = true, minWidth = 80, justify = "LEFT",
              headerTip = "Player",
              headerTipDesc = "The player affected by this event. For joins/leaves/quits this IS the actor (they did it to themselves). For kicks/promotes/demotes/invites it's the target the officer acted on. Column auto-sizes to fit the longest captured player name.",
              format = function(e) return fmtPlayer(e.target) end },

            { key = "rank", header = "Rank", autoFit = true, minWidth = 60, justify = "LEFT",
              headerTip = "Rank",
              headerTipDesc = "The player's guild rank at the time of the event. For promote / demote: the NEW rank after the action. For quit / leave / remove: the rank they had when they left or were kicked. For join: the guild's default (lowest-privilege) rank, synthesized from a live roster scan because Blizzard's API leaves `rank` empty on join events. For invite: deliberately blank — the invitee may decline the invite and never actually get a rank, so we wait for the matching join event before attributing one. Default-rank lookup only works for your current guild; alt-guild entries fall back to an empty cell.",
              format = function(e)
                  -- Promote / demote / quit / leave / remove: the API
                  -- populates `rank` correctly — the NEW rank for
                  -- promote/demote, the player's rank-at-time-of-leave
                  -- for quit/leave/remove. Show as-is in brand gold.
                  if e.rank and e.rank ~= ""
                     and e.type ~= "join" and e.type ~= "invite" then
                      return "|cffffd700" .. e.rank .. "|r"
                  end

                  -- Join: the player just landed at the guild's
                  -- default rank. Blizzard's API gives us nothing in
                  -- `rank` for join events, so look up the current-
                  -- guild default (lowest-privilege rank from the
                  -- roster) so the cell reflects what actually
                  -- happened. Limitation: only resolves for the
                  -- character's currently-active guild; alt-guild
                  -- entries fall back to empty.
                  if e.type == "join" then
                      local defaultRank =
                          addon.logCategories["guild"]:GetDefaultRankFor(e.guildKey)
                      if defaultRank then
                          return "|cffffd700" .. defaultRank .. "|r"
                      end
                      return ""
                  end

                  -- Invite: leave blank. Blizzard's `rank` field for
                  -- invites is the INVITER's rank, not the invitee's,
                  -- so we can't use it. And the invitee may decline
                  -- the invite and never join — synthesizing a
                  -- default rank here would be misleading. The
                  -- matching join event (if/when it lands) carries
                  -- the actual outcome.
                  return ""
              end },

            { key = "actor", header = "Actor", autoFit = true, minWidth = 80, justify = "LEFT",
              headerTip = "Actor",
              headerTipDesc = "The officer who performed the action for kicks / promotes / demotes / invites. For self-events (joins / leaves / quits) Blizzard's API doesn't return an officer because there isn't one — the player did it themselves — so the cell falls back to the player's own name rather than rendering blank. Column auto-sizes to fit the longest captured actor name.",
              format = function(e)
                  -- Blizzard's GetGuildEventInfo returns nil for player2 on
                  -- self-events (join / leave / quit), so e.actor is empty
                  -- for those rows. Showing the player's own name in the
                  -- Actor column for self-events keeps the column populated
                  -- across the table rather than rendering as a wall of
                  -- em-dashes for guilds dominated by joins / quits.
                  local actor = e.actor
                  if not actor or actor == "" then actor = e.target end
                  return fmtPlayer(actor)
              end },

            { key = "detail", header = "Detail", justify = "LEFT",
              -- No width = stretchy. With every other column auto-fit to
              -- its content, Detail is the ONLY column that absorbs
              -- whatever space is left — and the ONLY column that may
              -- truncate when the window is too narrow. Currently empty
              -- (reserved for future per-event annotations); the cell
              -- being blank means the visible "dead space" is just the
              -- stretchy reservoir, not a layout bug.
              headerTip = "Detail",
              headerTipDesc = "Event-specific extras. Currently empty for every event type (the rank moved to its own column); the column is preserved as the rightmost stretchy reservoir so the table layout stays stable when future event annotations are added. This is also the only column that may truncate when the window is too narrow — all others auto-size to fit their content.",
              format = function(e) return fmtDetail(e) end },
        },
    })
    rl:SetSort("ts", true)
    rl:SetData(entries)
    if self._search and self._search ~= "" then rl:SetSearch(self._search) end

    body:SetCallback("OnRelease", function() rl:Detach() end)
end
