--[[
    Commander 3.0 - Social Data Source

    Provides access to friends list and player search.
    Commands: friends, who

    Uses C_BattleNet and C_FriendList APIs
]]

local ADDON_NAME, C3 = ...

-- Use shared utilities
local Colorize = function(...) return C3.Colors.Colorize(...) end
local fuzzyMatch = function(...) return C3.StringUtils.fuzzyMatch(...) end

-------------------------------------------------------------------------------
-- Class Color Mapping (shared)
-------------------------------------------------------------------------------

local CLASS_COLORS = {
    WARRIOR = "C79C6E",
    PALADIN = "F58CBA",
    HUNTER = "ABD473",
    ROGUE = "FFF569",
    PRIEST = "FFFFFF",
    DEATHKNIGHT = "C41F3B",
    SHAMAN = "0070DE",
    MAGE = "69CCF0",
    WARLOCK = "9482C9",
    MONK = "00FF96",
    DRUID = "FF7D0A",
    DEMONHUNTER = "A330C9",
    EVOKER = "33937F",
}

-- Map localized class names to class file names
local CLASS_NAME_TO_FILE = {}
for i = 1, GetNumClasses() do
    local className, classFile = GetClassInfo(i)
    if className and classFile then
        CLASS_NAME_TO_FILE[className] = classFile
    end
end

-------------------------------------------------------------------------------
-- Friend Type Registration
-------------------------------------------------------------------------------

C3.Types:Register("Friend", {
    description = "Friend from friends list",
    source = "friends",
    properties = {
        { "name", "Display name" },
        { "characterName", "Character name (if online in WoW)" },
        { "battleTag", "Battle.net tag" },
        { "accountName", "Account name" },
        { "level", "Character level" },
        { "class", "Class file name: WARRIOR, MAGE, etc." },
        { "classID", "Class ID number" },
        { "zone", "Current zone (if online)" },
        { "gameText", "Game/activity text" },
        { "isOnline", "true if currently online" },
        { "isBattleNet", "true if Battle.net friend" },
        { "client", "Game client: WoW, D3, etc." },
        { "status", "0=online, 1=AFK, 2=DND" },
    },
})

C3.Formatter:Register("Friend", {
    format = function(obj)
        local nameColor = obj.isOnline and (CLASS_COLORS[obj.class] or "FFFFFF") or "888888"
        local displayName = obj.name or obj.characterName or obj.battleTag or "Unknown"
        local name = C3.Formatter:Colorize(displayName, nameColor)

        -- Status indicator
        local statusStr = ""
        if not obj.isOnline then
            statusStr = " " .. Colorize("[Offline]", "status.dim")
        elseif obj.status == 1 then
            statusStr = " " .. Colorize("[AFK]", "accent.warning")
        elseif obj.status == 2 then
            statusStr = " " .. Colorize("[DND]", "accent.error")
        end

        -- BNet icon
        local bnetIcon = obj.isBattleNet and (C3.Formatter:IconWithPath("Interface\\FriendsFrame\\BattleNet-BattleNetIcon") .. " ") or ""

        -- Zone/activity
        local zoneStr = ""
        if obj.isOnline and obj.zone and obj.zone ~= "" then
            zoneStr = string.format(" |cff00ccff@ %s|r", obj.zone)
        end

        -- Character name for BNet friends
        local charStr = ""
        if obj.isBattleNet and obj.characterName and obj.characterName ~= "" and obj.characterName ~= displayName then
            local charColor = CLASS_COLORS[obj.class] or "FFFFFF"
            charStr = string.format(" |cff%s(%s)|r", charColor, obj.characterName)
        end

        -- Level
        local levelStr = ""
        if obj.isOnline and obj.level and obj.level > 0 then
            levelStr = " " .. Colorize(string.format("[%d]", obj.level), "status.dim")
        end

        return bnetIcon .. name .. charStr .. levelStr .. zoneStr .. statusStr
    end,
    list = function(obj)
        local status = obj.isOnline and Colorize("+", "accent.success") or Colorize("-", "status.dim")
        return string.format("%s %s", status, obj.name or obj.characterName or "?")
    end,
    compact = function(obj)
        return obj.name or obj.characterName or obj.battleTag or "?"
    end
})

-------------------------------------------------------------------------------
-- WhoResult Type Registration
-------------------------------------------------------------------------------

C3.Types:Register("WhoResult", {
    description = "Player from /who search results",
    source = "who <query>",
    properties = {
        { "name", "Character name" },
        { "guild", "Guild name (empty string if none)" },
        { "level", "Character level (number)" },
        { "class", "Class file name: WARRIOR, MAGE, etc." },
        { "classID", "Class ID number" },
        { "race", "Race name: Orc, Human, etc." },
        { "zone", "Current zone location" },
        { "isGuildMember", "true if in your guild" },
        { "isGroupMember", "true if in your party/raid" },
    },
})

C3.Formatter:Register("WhoResult", {
    format = function(obj)
        local color = CLASS_COLORS[obj.class] or "FFFFFF"
        local name = C3.Formatter:Colorize(obj.name or "Unknown", color)

        local levelStr = " " .. Colorize(string.format("(%d)", obj.level or 0), "status.dim")
        local guildStr = obj.guild and obj.guild ~= ""
            and (" " .. Colorize("<" .. obj.guild .. ">", "accent.success")) or ""
        local zoneStr = obj.zone and obj.zone ~= ""
            and string.format(" |cff00ccff@ %s|r", obj.zone) or ""

        return name .. levelStr .. guildStr .. zoneStr
    end,
    list = function(obj)
        return string.format("%d %s - %s", obj.level or 0, obj.name or "?", obj.zone or "?")
    end,
    compact = function(obj)
        return obj.name or "?"
    end
})

-------------------------------------------------------------------------------
-- Players Intellisense Provider
-------------------------------------------------------------------------------

local MAX_SUGGESTIONS = C3.Constants.MAX_SUGGESTIONS

C3.Intellisense:RegisterProvider("Players", function(partial, ctx)
    local players = {}
    local seen = {}

    -- Add friends
    if C_FriendList and C_FriendList.GetNumOnlineFriends then
        local numFriends = C_FriendList.GetNumOnlineFriends() or 0
        for i = 1, numFriends do repeat
            local info = C_FriendList.GetFriendInfoByIndex(i)
            if not info or not info.connected or not info.name then break end

            local name = info.name:match("([^-]+)")
            if seen[name] or not fuzzyMatch(partial, name) then break end

            players[#players + 1] = { name = name, source = "Friend" }
            seen[name] = true
            if #players >= MAX_SUGGESTIONS then break end
        until true end
    end

    -- Add BNet friends playing WoW
    if #players < MAX_SUGGESTIONS and BNGetNumFriends and C_BattleNet and C_BattleNet.GetFriendAccountInfo then
        local numBNet = BNGetNumFriends() or 0
        for i = 1, numBNet do repeat
            local accountInfo = C_BattleNet.GetFriendAccountInfo(i)
            if not accountInfo or not accountInfo.gameAccountInfo then break end

            local gameInfo = accountInfo.gameAccountInfo
            if not gameInfo.isOnline or gameInfo.clientProgram ~= "WoW" or not gameInfo.characterName then break end

            local name = gameInfo.characterName
            if seen[name] or not fuzzyMatch(partial, name) then break end

            players[#players + 1] = { name = name, source = "BNet" }
            seen[name] = true
            if #players >= MAX_SUGGESTIONS then break end
        until true end
    end

    -- Add guild members
    if #players < MAX_SUGGESTIONS and IsInGuild and IsInGuild() then
        local numGuild = GetNumGuildMembers and GetNumGuildMembers() or 0
        for i = 1, numGuild do repeat
            local name, _, _, _, _, _, _, _, online = GetGuildRosterInfo(i)
            if not online or not name then break end

            local shortName = name:match("([^-]+)")
            if seen[shortName] or not fuzzyMatch(partial, shortName) then break end

            players[#players + 1] = { name = shortName, source = "Guild" }
            seen[shortName] = true
            if #players >= MAX_SUGGESTIONS then break end
        until true end
    end

    -- Add party/raid members
    if #players < MAX_SUGGESTIONS and IsInGroup and IsInGroup() then
        local numMembers = GetNumGroupMembers and GetNumGroupMembers() or 0
        local isRaid = IsInRaid and IsInRaid()
        for i = 1, numMembers do repeat
            local unit = isRaid and ("raid" .. i) or ("party" .. i)
            local name = UnitName(unit)
            if not name or seen[name] or not fuzzyMatch(partial, name) then break end

            players[#players + 1] = { name = name, source = "Group" }
            seen[name] = true
            if #players >= MAX_SUGGESTIONS then break end
        until true end
    end

    local results = {}
    for _, p in ipairs(players) do
        results[#results + 1] = {
            text = p.name,
            description = p.source,
        }
    end
    return results
end)

-------------------------------------------------------------------------------
-- friends - List online friends
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "friends",
    description = "List online friends",
    category = "data",
    producesType = "Friend",
    acceptsInput = false,
    flags = {
        all = { short = "a", description = "Show all friends (including offline)" },
        bnet = { short = "b", description = "Show only Battle.net friends" },
        wow = { short = "w", description = "Show only WoW friends" },
    },
    execute = function(ctx)
        local showAll = ctx:HasFlag("all") or ctx:HasFlag("a")
        local bnetOnly = ctx:HasFlag("bnet") or ctx:HasFlag("b")
        local wowOnly = ctx:HasFlag("wow") or ctx:HasFlag("w")

        -- Build results array with yielding to prevent freezing
        -- We can't use ctx:Produce() because coroutine.wrap() creates a C-call
        -- boundary that prevents yielding during iteration
        local results = {}
        local processed = 0
        local YIELD_INTERVAL = C3.Constants.YIELD_INTERVAL

        -- Battle.net friends
        if not wowOnly then
            local numBNet = BNGetNumFriends and BNGetNumFriends() or 0

            for i = 1, numBNet do
                -- Yield periodically to prevent freezing
                processed = processed + 1
                if processed % YIELD_INTERVAL == 0 then
                    coroutine.yield()
                end

                local accountInfo = C_BattleNet and C_BattleNet.GetFriendAccountInfo and
                    C_BattleNet.GetFriendAccountInfo(i)

                if accountInfo then
                    local gameInfo = accountInfo.gameAccountInfo or {}
                    local isOnline = gameInfo.isOnline or false

                    if showAll or isOnline then
                        local classFile = nil
                        if gameInfo.className then
                            classFile = CLASS_NAME_TO_FILE[gameInfo.className]
                        end

                        -- Determine display name and zone
                        local displayName = accountInfo.accountName or accountInfo.battleTag
                        local zone = gameInfo.areaName or gameInfo.richPresence or ""

                        -- Determine status
                        local status = 0
                        if accountInfo.isAFK then
                            status = 1
                        elseif accountInfo.isDND then
                            status = 2
                        end

                        table.insert(results, C3.Types.Create("Friend", {
                            name = displayName,
                            characterName = gameInfo.characterName,
                            battleTag = accountInfo.battleTag,
                            accountName = accountInfo.accountName,
                            level = gameInfo.characterLevel,
                            class = classFile,
                            classID = gameInfo.classID,
                            zone = zone,
                            gameText = gameInfo.clientProgram,
                            isOnline = isOnline,
                            isBattleNet = true,
                            client = gameInfo.clientProgram,
                            status = status,
                        }))
                    end
                end
            end
        end

        -- Traditional WoW friends
        if not bnetOnly then
            local numWoW = C_FriendList and C_FriendList.GetNumFriends and
                C_FriendList.GetNumFriends() or 0

            for i = 1, numWoW do
                -- Yield periodically to prevent freezing
                processed = processed + 1
                if processed % YIELD_INTERVAL == 0 then
                    coroutine.yield()
                end

                local info = C_FriendList.GetFriendInfoByIndex(i)

                if info then
                    local isOnline = info.connected or false

                    if showAll or isOnline then
                        local classFile = nil
                        if info.className then
                            classFile = CLASS_NAME_TO_FILE[info.className]
                        end

                        local status = 0
                        if info.afk then
                            status = 1
                        elseif info.dnd then
                            status = 2
                        end

                        table.insert(results, C3.Types.Create("Friend", {
                            name = info.name,
                            characterName = info.name,
                            battleTag = nil,
                            accountName = nil,
                            level = info.level,
                            class = classFile,
                            classID = nil,
                            zone = info.area,
                            gameText = "WoW",
                            isOnline = isOnline,
                            isBattleNet = false,
                            client = "WoW",
                            status = status,
                        }))
                    end
                end
            end
        end

        return results
    end
})

-------------------------------------------------------------------------------
-- Who Query Processing
-------------------------------------------------------------------------------

-- Class shortcuts for who queries
local CLASS_SHORTCUTS = {
    warrior = "Warrior", war = "Warrior", warr = "Warrior",
    paladin = "Paladin", pal = "Paladin", pala = "Paladin",
    hunter = "Hunter", hunt = "Hunter",
    rogue = "Rogue", rog = "Rogue",
    priest = "Priest", pri = "Priest",
    dk = "Death Knight", deathknight = "Death Knight",
    shaman = "Shaman", sham = "Shaman",
    mage = "Mage",
    warlock = "Warlock", lock = "Warlock",
    monk = "Monk",
    druid = "Druid", dru = "Druid",
    dh = "Demon Hunter", demonhunter = "Demon Hunter",
    evoker = "Evoker", evok = "Evoker",
}

-- Role to classes mapping
local ROLE_CLASSES = {
    tank = { "Warrior", "Paladin", "Death Knight", "Monk", "Druid", "Demon Hunter" },
    healer = { "Priest", "Paladin", "Shaman", "Monk", "Druid", "Evoker" },
    dps = { "Warrior", "Hunter", "Rogue", "Mage", "Warlock", "Demon Hunter", "Evoker" },
    melee = { "Warrior", "Paladin", "Rogue", "Death Knight", "Monk", "Demon Hunter" },
    ranged = { "Hunter", "Mage", "Warlock", "Priest", "Evoker" },
}

-- Zone shortcuts
local ZONE_SHORTCUTS = {
    org = "Orgrimmar",
    sw = "Stormwind",
    stormwind = "Stormwind",
    dal = "Dalaran",
    valdrakken = "Valdrakken",
    vald = "Valdrakken",
    dorn = "Dornogal",
    dornogal = "Dornogal",
    ["if"] = "Ironforge",
    ironforge = "Ironforge",
    tb = "Thunder Bluff",
    uc = "Undercity",
    silvermoon = "Silvermoon",
    exodar = "Exodar",
    oribos = "Oribos",
}

---Get the current zone name suitable for /who queries
---@return string Zone name
local function getCurrentZoneName()
    -- Walk up the map hierarchy to find Zone-level map (not subzone/floor)
    local mapID = C_Map.GetBestMapForUnit("player")
    local depth = 0
    local MAX_DEPTH = 20  -- Safety limit to prevent infinite loop on circular references
    while mapID and depth < MAX_DEPTH do
        depth = depth + 1
        local mapInfo = C_Map.GetMapInfo(mapID)
        if mapInfo then
            -- mapType 3 = Zone (not Dungeon, Micro, etc.)
            if mapInfo.mapType == Enum.UIMapType.Zone then
                return mapInfo.name
            end
            -- Walk up to parent map
            mapID = mapInfo.parentMapID
        else
            break
        end
    end
    -- Fallback to zone text APIs
    return GetZoneText() or GetRealZoneText() or ""
end

---Process who query and expand shortcuts
---@param query string Raw query string
---@return string Processed query for WoW /who
local function processWhoQuery(query)
    if not query or query == "" then return "" end

    -- Special case: "." means current zone
    if query == "." then
        return "z-\"" .. getCurrentZoneName() .. "\""
    end

    -- Normalize: remove spaces around comparison operators
    query = query:gsub("<%s+", "<"):gsub(">%s+", ">")
    query = query:gsub("<=%s+", "<="):gsub(">=%s+", ">=")

    local parts = {}

    for word in query:gmatch("%S+") do
        local lower = word:lower()

        -- "." means current zone
        if word == "." then
            table.insert(parts, "z-\"" .. getCurrentZoneName() .. "\"")
        -- Class shortcuts
        elseif CLASS_SHORTCUTS[lower] then
            table.insert(parts, "c-\"" .. CLASS_SHORTCUTS[lower] .. "\"")
        -- Role shortcuts (use first class from role)
        elseif ROLE_CLASSES[lower] then
            local classes = ROLE_CLASSES[lower]
            if classes and classes[1] then
                table.insert(parts, "c-\"" .. classes[1] .. "\"")
            end
        -- Zone shortcuts
        elseif ZONE_SHORTCUTS[lower] then
            table.insert(parts, "z-\"" .. ZONE_SHORTCUTS[lower] .. "\"")
        -- Level range shortcuts (+-N means player level +/- N)
        elseif word:match("^%+%-%d+$") then
            local range = tonumber(word:match("%d+"))
            local playerLevel = UnitLevel("player")
            local maxLevel = GetMaxPlayerLevel and GetMaxPlayerLevel() or 80
            table.insert(parts, string.format("%d-%d",
                math.max(1, playerLevel - range),
                math.min(maxLevel, playerLevel + range)
            ))
        -- Level comparison shortcuts: <80, >70, <=79, >=70
        elseif word:match("^<=%d+$") then
            local level = tonumber(word:match("%d+"))
            table.insert(parts, string.format("1-%d", level))
        elseif word:match("^>=%d+$") then
            local level = tonumber(word:match("%d+"))
            local maxLevel = GetMaxPlayerLevel and GetMaxPlayerLevel() or 80
            table.insert(parts, string.format("%d-%d", level, maxLevel))
        elseif word:match("^<%d+$") then
            local level = tonumber(word:match("%d+"))
            table.insert(parts, string.format("1-%d", level - 1))
        elseif word:match("^>%d+$") then
            local level = tonumber(word:match("%d+"))
            local maxLevel = GetMaxPlayerLevel and GetMaxPlayerLevel() or 80
            table.insert(parts, string.format("%d-%d", level + 1, maxLevel))
        else
            table.insert(parts, word)
        end
    end

    return table.concat(parts, " ")
end

-------------------------------------------------------------------------------
-- Who Results Listener
-- Listens for WHO_LIST_UPDATE to format results nicely
-------------------------------------------------------------------------------

-- Pending who query state (for filtering)
local pendingWhoFilters = nil

-- Cache of last who results for pipeline access
local lastWhoResults = {}

-- Flag to track if Commander triggered the who search
local whoTriggeredByCommander = false
-- Track if Who pane was already open before we triggered the search
local whoPaneWasAlreadyOpen = false
-- Pipeline continuation to execute after who results arrive
local pendingWhoContinuation = nil

---Mark that Commander is about to trigger a /who command
---Called from SecureAction before executing /who
function C3.Social_MarkWhoFromCommander()
    whoTriggeredByCommander = true
    -- Remember if the Who pane is already open - don't close it if user opened it manually
    whoPaneWasAlreadyOpen = (WhoFrame and WhoFrame:IsShown()) or
        (FriendsFrame and FriendsFrame:IsShown() and
         PanelTemplates_GetSelectedTab and
         PanelTemplates_GetSelectedTab(FriendsFrame) == 2)
end

---Set pipeline continuation for deferred execution after who results
---@param continuation string The pipeline text after 'who ...' (e.g., "where level == 80")
function C3.Social_SetWhoContinuation(continuation)
    pendingWhoContinuation = continuation
end

---Clear any pending pipeline continuation
function C3.Social_ClearWhoContinuation()
    pendingWhoContinuation = nil
end

-- Frame for handling who results
local whoEventFrame = CreateFrame("Frame")
whoEventFrame:RegisterEvent("WHO_LIST_UPDATE")
whoEventFrame:SetScript("OnEvent", function(self, event)
    -- Only intercept if Commander triggered this who search
    if not whoTriggeredByCommander then
        return  -- Let the default UI handle it normally
    end

    -- Clear the flag
    whoTriggeredByCommander = false

    -- Hide the default Who frame since Commander will display results
    -- BUT only if the user didn't have it open already
    local terminal = C3.Terminal
    if terminal and terminal.frame and terminal.frame:IsVisible() and not whoPaneWasAlreadyOpen then
        if WhoFrame and WhoFrame:IsShown() then
            WhoFrame:Hide()
        end
        -- Also hide the Friends frame Who tab if it's showing
        if FriendsFrame and FriendsFrame:IsShown() then
            if PanelTemplates_GetSelectedTab and FriendsFrame.numTabs then
                local selectedTab = PanelTemplates_GetSelectedTab(FriendsFrame)
                if selectedTab == 2 then  -- Who tab is typically tab 2
                    FriendsFrame:Hide()
                end
            end
        end
    end

    -- Reset the flag for next time
    whoPaneWasAlreadyOpen = false

    -- Get player's guild for filtering
    local playerGuild = GetGuildInfo("player")
    local filters = pendingWhoFilters or {}
    pendingWhoFilters = nil

    -- Collect results
    local results = {}
    local numResults = C_FriendList.GetNumWhoResults()

    if numResults == 0 then
        -- Clear cache
        lastWhoResults = {}

        -- If there's a pipeline continuation, still execute it (will produce empty result)
        if pendingWhoContinuation then
            local continuation = pendingWhoContinuation
            pendingWhoContinuation = nil
            local pipelineCommand = "whoresults | " .. continuation
            C_Timer.After(0, function()
                if C3.Execute then
                    C3:Execute(pipelineCommand)
                end
            end)
            return
        end

        -- Show message if no continuation
        local terminal = C3.Terminal
        if terminal and terminal.output and terminal.frame and terminal.frame:IsVisible() then
            terminal.output:AddLine(Colorize("No players found.", "status.dim"))
            terminal.output:ScrollToBottom()
        end
        return
    end

    for i = 1, numResults do
        local info = C_FriendList.GetWhoInfo(i)

        if info then
            -- Apply post-filters
            local include = true

            if filters.excludeGuild and info.fullGuildName and info.fullGuildName == playerGuild then
                include = false
            end

            if filters.excludeGroup then
                local inGroup = UnitInParty(info.fullName) or UnitInRaid(info.fullName)
                if inGroup then
                    include = false
                end
            end

            if include then
                results[#results + 1] = C3.Types.Create("WhoResult", {
                    name = info.fullName,
                    guild = info.fullGuildName,
                    level = info.level,
                    class = info.filename,
                    classID = info.classID,
                    race = info.raceStr,
                    zone = info.area,
                    isGuildMember = info.fullGuildName and info.fullGuildName == playerGuild,
                    isGroupMember = UnitInParty(info.fullName) or UnitInRaid(info.fullName) or false,
                })
            end
        end
    end

    -- Cache results for pipeline access via 'whoresults' command
    lastWhoResults = results

    -- Check for deferred pipeline continuation
    if pendingWhoContinuation then
        local continuation = pendingWhoContinuation
        pendingWhoContinuation = nil

        -- Execute the continuation pipeline using cached results
        -- Build command: whoresults | <continuation>
        local pipelineCommand = "whoresults | " .. continuation

        -- Use C_Timer.After to avoid executing during event handler
        C_Timer.After(0, function()
            local terminal = C3.Terminal
            if terminal and terminal.ExecuteCommand then
                -- Use terminal's execute which handles output display
                terminal:ExecuteCommand(pipelineCommand)
            elseif C3.Execute then
                -- Fallback: execute and manually display results
                local task = C3:Execute(pipelineCommand)
                if task and task.OnComplete then
                    task:OnComplete(function(_, result)
                        if result and terminal and terminal.output then
                            terminal:_handleResult(result)
                        end
                    end)
                end
            end
        end)
        return  -- Don't output results directly - pipeline will handle it
    end

    -- Output results to the terminal if open (no pipeline continuation)
    local terminal = C3.Terminal
    if terminal and terminal.output and terminal.frame and terminal.frame:IsVisible() then
        local output = terminal.output

        -- Add header
        output:AddLine(Colorize(string.format("Found %d player(s):", #results), "accent.success"))

        -- Add each result
        for _, result in ipairs(results) do
            local formatted = C3.Formatter:Format(result)
            output:AddLine(formatted)
        end

        -- Scroll to bottom
        output:ScrollToBottom()
    end
end)

-------------------------------------------------------------------------------
-- who - Search for online players
-- Note: This command uses SecureAction to execute /who via Enter key
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "who",
    description = "Search for online players",
    category = "data",
    acceptsInput = false,
    args = {
        { name = "query", required = false, vararg = true,
          description = "Search query (. = current zone, class names, zone shortcuts, level ranges)" }
    },
    flags = {
        noguild = { short = "g", description = "Exclude guild members from results" },
        nogroup = { description = "Exclude group members from results" },
    },
    execute = function(ctx)
        -- Build query from args
        local rawArgs = ctx:GetArgs()
        local args = {}
        for i, v in ipairs(rawArgs) do
            args[i] = tostring(v or "")
        end
        local query = table.concat(args, " ")

        local excludeGuild = ctx:HasFlag("noguild") or ctx:HasFlag("g")
        local excludeGroup = ctx:HasFlag("nogroup")

        -- Process query shortcuts
        local processedQuery = processWhoQuery(query)

        -- Store filters for when results come in
        if excludeGuild or excludeGroup then
            pendingWhoFilters = {
                excludeGuild = excludeGuild,
                excludeGroup = excludeGroup,
            }
        end

        -- If we got here, the secure action wasn't armed (user typed too fast)
        -- Tell them to retype
        return ctx:Text("Type 'who " .. query .. "' and press Enter to search")
    end
})

-------------------------------------------------------------------------------
-- whoresults - Access last who search results for pipeline operations
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "whoresults",
    description = "Returns last who search results (for pipeline filtering)",
    category = "data",
    acceptsInput = false,
    execute = function(ctx)
        if #lastWhoResults == 0 then
            return ctx:Error("No who results cached. Run 'who <query>' first.")
        end

        -- Return a copy of the results array directly
        local results = {}
        for i, result in ipairs(lastWhoResults) do
            results[i] = result
        end
        return results
    end
})

-------------------------------------------------------------------------------
-- invite - Invite player to group
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "invite",
    description = "Invite player to group",
    category = "action",
    args = {
        { name = "player", required = false, description = "Player name to invite",
          provider = "Players" }
    },
    execute = function(ctx)
        -- Check for pipeline input
        if ctx:HasInput() then
            local input = ctx:GetInput()
            -- Pipeline mode: invite from piped objects
            local invited = {}

            -- Handle both iterator (function) and array (table) input
            if type(input) == "function" then
                for obj in input do
                    local name = obj.name or obj.characterName or obj.fullName
                    if name then
                        C_PartyInfo.InviteUnit(name)
                        invited[#invited + 1] = name
                    end
                end
            elseif type(input) == "table" then
                -- Could be a single object or an array
                local items = input._type and { input } or input
                for _, obj in ipairs(items) do
                    local name = obj.name or obj.characterName or obj.fullName
                    if name then
                        C_PartyInfo.InviteUnit(name)
                        invited[#invited + 1] = name
                    end
                end
            end

            if #invited > 0 then
                -- Return array directly instead of using ctx:Produce()
                local results = {}
                for _, name in ipairs(invited) do
                    table.insert(results, C3.Types.Create("Text", {
                        content = "Invited: " .. name
                    }))
                end
                return results
            else
                return ctx:Error("No valid players to invite")
            end
        end

        -- Argument mode
        local player = ctx:GetArg(1)
        if not player or player == "" then
            return ctx:Error("Usage: invite <player> or who ... | invite")
        end

        C_PartyInfo.InviteUnit(player)
        return C3.Types.Create("Text", {
            content = "Invited: " .. player
        })
    end
})

-------------------------------------------------------------------------------
-- whisper - Send whisper to player
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "whisper",
    description = "Send whisper to player",
    category = "action",
    acceptsInput = false,
    args = {
        { name = "player", required = true, description = "Player name",
          provider = "Players" },
        { name = "message", required = true, vararg = true, description = "Message to send" }
    },
    execute = function(ctx)
        local player = ctx:GetArg(1)
        if not player or player == "" then
            return ctx:Error("Usage: whisper <player> <message>")
        end

        -- Gather remaining args as message
        local rawArgs = ctx:GetArgs()
        local args = {}
        for i = 2, #rawArgs do  -- Skip player name (index 1)
            args[#args + 1] = tostring(rawArgs[i] or "")
        end
        local message = table.concat(args, " ")

        if message == "" then
            return ctx:Error("Usage: whisper <player> <message>")
        end

        SendChatMessage(message, "WHISPER", nil, player)
        return C3.Types.Create("Text", {
            content = string.format("Whispered %s: %s", player, message)
        })
    end
})

-------------------------------------------------------------------------------
-- ready - Initiate ready check
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "ready",
    description = "Initiate a ready check",
    category = "action",
    acceptsInput = false,
    execute = function(ctx)
        if not IsInGroup() then
            return ctx:Error("Not in a group")
        end
        if not UnitIsGroupLeader("player") and not UnitIsGroupAssistant("player") then
            return ctx:Error("Must be leader or assistant to initiate ready check")
        end
        DoReadyCheck()
        return ctx:Text("Ready check initiated")
    end
})

-------------------------------------------------------------------------------
-- roll - Random roll
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "roll",
    description = "Roll random number (default 1-100)",
    category = "action",
    acceptsInput = false,
    args = {
        { name = "min", required = false, description = "Minimum value (default 1)" },
        { name = "max", required = false, description = "Maximum value (default 100)" },
    },
    execute = function(ctx)
        local arg1 = tonumber(ctx:GetArg(1))
        local arg2 = tonumber(ctx:GetArg(2))

        local min, max
        if arg1 and arg2 then
            min, max = arg1, arg2
        elseif arg1 then
            min, max = 1, arg1
        else
            min, max = 1, 100
        end

        RandomRoll(min, max)
        return nil  -- WoW displays roll in chat
    end
})

-------------------------------------------------------------------------------
-- promote - Promote player to group leader
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "promote",
    description = "Promote player to group leader",
    category = "action",
    acceptsInput = false,
    args = {
        {
            name = "player",
            required = true,
            description = "Player name to promote",
            provider = "Players"
        }
    },
    execute = function(ctx)
        local player = ctx:GetArg(1)
        if not player or player == "" then
            return ctx:Error("Usage: promote <player>")
        end

        if not IsInGroup() then
            return ctx:Error("Not in a group")
        end

        if not UnitIsGroupLeader("player") then
            return ctx:Error("Must be group leader to promote")
        end

        PromoteToLeader(player)
        return ctx:Text("Promoted " .. player .. " to leader")
    end
})

-------------------------------------------------------------------------------
-- kick - Remove player from group
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "kick",
    description = "Remove player from group",
    category = "action",
    acceptsInput = false,
    args = {
        {
            name = "player",
            required = true,
            description = "Player name to remove",
            provider = "Players"
        }
    },
    execute = function(ctx)
        local player = ctx:GetArg(1)
        if not player or player == "" then
            return ctx:Error("Usage: kick <player>")
        end

        if not IsInGroup() then
            return ctx:Error("Not in a group")
        end

        UninviteUnit(player)
        return ctx:Text("Removed " .. player .. " from group")
    end
})

-------------------------------------------------------------------------------
-- leave - Leave current group
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "leave",
    description = "Leave current group",
    category = "action",
    acceptsInput = false,
    execute = function(ctx)
        if not IsInGroup() then
            return ctx:Error("Not in a group")
        end

        LeaveParty()
        return ctx:Text("Left group")
    end
})

-------------------------------------------------------------------------------
-- inspect - Inspect a player
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "inspect",
    description = "Inspect a player",
    category = "action",
    acceptsInput = false,
    args = {
        {
            name = "unit",
            required = false,
            description = "Unit to inspect (default: target)",
        }
    },
    execute = function(ctx)
        local unit = ctx:GetArg(1) or "target"

        if not UnitExists(unit) then
            return ctx:Error("Unit not found: " .. unit)
        end

        if not UnitIsPlayer(unit) then
            return ctx:Error("Can only inspect players")
        end

        if not CanInspect(unit) then
            return ctx:Error("Cannot inspect: too far away or not visible")
        end

        InspectUnit(unit)
        local name = UnitName(unit) or unit
        return ctx:Text("Inspecting " .. name)
    end
})

-------------------------------------------------------------------------------
-- Chat Shortcuts
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "say",
    description = "Say something in local chat",
    category = "action",
    acceptsInput = false,
    args = {
        { name = "message", required = true, vararg = true, description = "Message to say" }
    },
    execute = function(ctx)
        local args = ctx:GetArgs()
        local message = table.concat(args, " ")
        if message == "" then
            return ctx:Error("Usage: say <message>")
        end
        SendChatMessage(message, "SAY")
        return nil  -- Chat output appears in chat frame
    end
})

C3.Command:Define({
    name = "yell",
    description = "Yell something",
    category = "action",
    acceptsInput = false,
    args = {
        { name = "message", required = true, vararg = true, description = "Message to yell" }
    },
    execute = function(ctx)
        local args = ctx:GetArgs()
        local message = table.concat(args, " ")
        if message == "" then
            return ctx:Error("Usage: yell <message>")
        end
        SendChatMessage(message, "YELL")
        return nil
    end
})

C3.Command:Define({
    name = "p",
    description = "Send message to party/raid chat",
    category = "action",
    acceptsInput = false,
    args = {
        { name = "message", required = true, vararg = true, description = "Message to send" }
    },
    execute = function(ctx)
        local args = ctx:GetArgs()
        local message = table.concat(args, " ")
        if message == "" then
            return ctx:Error("Usage: p <message>")
        end
        if not IsInGroup() then
            return ctx:Error("Not in a group")
        end
        local channel = IsInRaid() and "RAID" or "PARTY"
        SendChatMessage(message, channel)
        return nil
    end
})

C3.Command:Define({
    name = "g",
    description = "Send message to guild chat",
    category = "action",
    acceptsInput = false,
    args = {
        { name = "message", required = true, vararg = true, description = "Message to send" }
    },
    execute = function(ctx)
        local args = ctx:GetArgs()
        local message = table.concat(args, " ")
        if message == "" then
            return ctx:Error("Usage: g <message>")
        end
        if not IsInGuild() then
            return ctx:Error("Not in a guild")
        end
        SendChatMessage(message, "GUILD")
        return nil
    end
})

-------------------------------------------------------------------------------
-- Status Commands
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "afk",
    description = "Toggle or set AFK status",
    category = "action",
    acceptsInput = false,
    args = {
        { name = "message", required = false, vararg = true, description = "AFK message (optional)" }
    },
    execute = function(ctx)
        local args = ctx:GetArgs()
        local message = table.concat(args, " ")
        SendChatMessage(message, "AFK")
        if UnitIsAFK("player") then
            return ctx:Text("AFK status cleared")
        else
            return ctx:Text("AFK status set" .. (message ~= "" and (": " .. message) or ""))
        end
    end
})

C3.Command:Define({
    name = "dnd",
    description = "Toggle or set Do Not Disturb status",
    category = "action",
    acceptsInput = false,
    args = {
        { name = "message", required = false, vararg = true, description = "DND message (optional)" }
    },
    execute = function(ctx)
        local args = ctx:GetArgs()
        local message = table.concat(args, " ")
        SendChatMessage(message, "DND")
        if UnitIsDND("player") then
            return ctx:Text("DND status cleared")
        else
            return ctx:Text("DND status set" .. (message ~= "" and (": " .. message) or ""))
        end
    end
})

return true
