-- TOG Tools — Guild Log sub-tab (UI)
--
-- Renders inside the Logs outer tab's nested AceGUI TabGroup. Layout:
--   Filter row: Guild ▾   Type ▾   Date range ▾   [Refresh]   [Clear]
--   Data table: Time · Guild · Type · Target · Actor · Detail
--
-- Refresh button calls GuildLog:Requery() — issues a fresh
-- GuildEventLog_QueryGuild and re-ingests; useful when the user wants
-- the very latest without waiting for the 30-minute periodic ticker.
--
-- 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._filterType         = GuildLog._filterType         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

-- ---------------------------------------------------------------------------
-- 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
-- ---------------------------------------------------------------------------
local function fmtTime(ts)
    if not ts or ts <= 0 then return "—" end
    local now = (GetServerTime and GetServerTime()) or time()
    local diff = now - ts
    if diff < 3600  then return "Just now" end
    if diff < 86400 then return string.format("%dh ago", math.floor(diff / 3600)) end
    -- Older entries: show date AND time so users can pinpoint when an
    -- event happened, not just which day. Blizzard's API exposes hour
    -- precision only, so minute is always :00 — embedded explicitly
    -- in the format string (no point reading a 0 from the underlying
    -- ts that's already hour-rounded). Year shows for entries older
    -- than ~11 months so the time column isn't ambiguous across years.
    if diff < 330 * 86400 then
        return date("%m/%d %H:00", ts)
    end
    return date("%m/%d/%y %H:00", ts)
end

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

local function fmtGuild(key)
    if not key or key == "" then return "|cff888888?|r" end
    -- Strip the "-Realm" suffix when the realm matches the player's own
    -- — keeps the column compact for the single-realm common case.
    local own = (GetRealmName() or ""):gsub("%s+", "")
    local guildName, realm = key:match("^(.+)%-([^%-]+)$")
    if guildName and realm == own then return guildName end
    return key
end

-- ---------------------------------------------------------------------------
-- (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
-- ---------------------------------------------------------------------------
local function buildGuildDropdown(onChange)
    local dd = AceGUI:Create("Dropdown")
    dd:SetLabel("Guild")
    dd:SetWidth(180)
    local current = GuildLog:GetCurrentGuildKey()
    local list, order = { __all = "All guilds" }, { "__all" }
    if current then
        list[current] = fmtGuild(current) .. " (current)"
        tinsert(order, current)
    end
    for _, k in ipairs(GuildLog:GetGuildKeys()) do
        if k ~= current then
            list[k] = fmtGuild(k)
            tinsert(order, k)
        end
    end
    -- Default selection: current guild if known and present in the
    -- list, else "All guilds". GuildLog._filterGuild can override.
    local default = GuildLog._filterGuild or current or "__all"
    if not list[default] then default = "__all" end
    dd:SetList(list, order)
    dd:SetValue(default)
    dd:SetCallback("OnValueChanged", function(_, _, key)
        GuildLog._filterGuild = key
        onChange()
    end)
    return dd
end

local function buildTypeDropdown(onChange)
    local dd = AceGUI:Create("Dropdown")
    dd:SetLabel("Type")
    dd:SetWidth(140)
    dd:SetList(
        { __all = "All types", join = "Joined", leave = "Left",
          remove = "Kicked", promote = "Promoted", demote = "Demoted",
          quit = "Quit", invite = "Invited" },
        { "__all", "join", "leave", "quit", "remove", "promote", "demote", "invite" })
    dd:SetValue(GuildLog._filterType or "__all")
    dd:SetCallback("OnValueChanged", function(_, _, key)
        GuildLog._filterType = (key == "__all") and nil or key
        onChange()
    end)
    return dd
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

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

    filterRow:AddChild(buildGuildDropdown(rebuild))
    filterRow:AddChild(buildTypeDropdown(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,
    }))

    -- (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, self._filterType, 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, self._filterType, 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 date range / type filter."
        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 visible cell on each SetData call,
    -- growing col.width to fit. 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.
    local 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 visible 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 visible guild name.",
              format = function(e) return fmtGuild(e.guildKey) end },

            { key = "type", header = "Type", autoFit = true, minWidth = 60, justify = "LEFT",
              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 visible 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 visible 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)

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