--[[
    Commander 3.0 - Query Operators

    Pipeline transformation commands: where, sort, first, last, take, skip, select,
    count, sum, avg, min, max, random, unique, group, concat
    These operate on the pipeline stream of objects.
]]

local ADDON_NAME, C3 = ...

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

-------------------------------------------------------------------------------
-- Shared Helpers
-------------------------------------------------------------------------------

---Extract a numeric value from an object for aggregation (sum, avg, min, max)
---@param obj any The object to extract from
---@param field string? Optional field name to extract
---@return number? value The extracted numeric value, or nil
local function extractNumericValue(obj, field)
    local value
    if field and type(obj) == "table" then
        value = obj[field]
    elseif type(obj) == "number" then
        value = obj
    elseif type(obj) == "table" and obj.value then
        value = obj.value
    end
    return tonumber(value)
end

-------------------------------------------------------------------------------
-- Expression Evaluator (now uses recursive descent parser)
-------------------------------------------------------------------------------

-- The ExpressionParser (core/expression_parser.lua) handles:
-- - Proper operator precedence (and binds tighter than or)
-- - Parentheses grouping: (a or b) and c
-- - Nested property access: stats.intellect > 100
-- - Boolean operators: and, or, not
-- - Comparison operators: >, >=, <, <=, ==, !=, ~= (contains)

-------------------------------------------------------------------------------
-- where - Filter objects
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "where",
    description = "Filter objects by expression",
    category = "query",
    acceptsInput = true,
    passthrough = true,  -- Preserves input type
    args = {
        { name = "expression", required = true, description = "Filter expression (e.g., 'quality > 3')" }
    },
    execute = function(ctx)
        -- Join all args into single expression, preserving string quotes for proper parsing
        local expr = table.concat(ctx:GetExprArgs(), " ")
        if not expr or expr == "" then
            return ctx:Error("Usage: where <expression>")
        end

        if not ctx:HasInput() then
            return ctx:Error("where requires pipeline input")
        end

        -- Validate expression upfront before iterating
        local ast, parseErr = C3.ExpressionParser:Parse(expr)
        if not ast then
            return ctx:Error("Invalid expression: " .. (parseErr or "unknown error"))
        end

        return ctx:Transform(function(obj)
            if type(obj) ~= "table" then
                return nil
            end

            -- Get the data to evaluate
            local data = obj._type and obj or obj

            -- Use the new ExpressionParser for proper precedence and grouping
            if C3.ExpressionParser:Evaluate(expr, data) then
                return obj
            end
            return nil
        end)
    end
})

-------------------------------------------------------------------------------
-- sort - Sort objects
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "sort",
    description = "Sort objects by field",
    category = "query",
    acceptsInput = true,
    passthrough = true,  -- Preserves input type
    args = {
        { name = "field", required = false, description = "Field to sort by" }
    },
    flags = {
        desc = { short = "d", description = "Sort descending" },
        numeric = { short = "n", description = "Numeric sort" },
    },
    execute = function(ctx)
        local field = ctx:GetArg(1)
        local direction = ctx:GetArg(2)

        -- Support positional asc/desc (e.g., "sort ilvl desc") as well as flags
        local desc = ctx:HasFlag("desc") or ctx:HasFlag("d")
                  or direction == "desc" or direction == "descending"
        local numeric = ctx:HasFlag("numeric") or ctx:HasFlag("n")

        if not ctx:HasInput() then
            return ctx:Error("sort requires pipeline input")
        end

        -- Collect all items first (sorting requires full materialization)
        local items = C3.Pipeline.Collect(ctx:GetInput())

        -- Sort
        table.sort(items, function(a, b)
            local aVal, bVal

            if field then
                aVal = type(a) == "table" and a[field]
                bVal = type(b) == "table" and b[field]
            else
                aVal = type(a) == "table" and (a.name or a.display or a[1]) or a
                bVal = type(b) == "table" and (b.name or b.display or b[1]) or b
            end

            -- Handle nil
            if aVal == nil then return false end
            if bVal == nil then return true end

            -- Numeric conversion
            if numeric then
                aVal = tonumber(aVal) or 0
                bVal = tonumber(bVal) or 0
            end

            -- Compare
            if desc then
                return aVal > bVal
            else
                return aVal < bVal
            end
        end)

        -- Return sorted array directly (not iterator)
        return items
    end
})

-------------------------------------------------------------------------------
-- first - Take first N items (or just first 1)
-- Uses short-circuit iteration - stops pulling from upstream after N items
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "first",
    description = "Take first N items",
    category = "query",
    acceptsInput = true,
    passthrough = true,  -- Preserves input type
    args = {
        { name = "count", required = false, description = "Number of items (default 1)" }
    },
    execute = function(ctx)
        local count = tonumber(ctx:GetArg(1)) or 1

        if not ctx:HasInput() then
            return ctx:Error("first requires pipeline input")
        end

        -- Short-circuit: stop pulling from upstream after we have enough
        local input = ctx:GetInput()
        local iter = type(input) == "function" and input or C3.Pipeline.Collect(input)

        if type(iter) == "table" then
            -- Input is array, just slice it
            local result = {}
            for i = 1, math.min(count, #iter) do
                result[i] = iter[i]
            end
            return result
        end

        -- Input is iterator - create short-circuit iterator
        return coroutine.wrap(function()
            local taken = 0
            for obj in iter do
                if taken >= count then
                    return  -- Stop pulling from upstream!
                end
                coroutine.yield(obj)
                taken = taken + 1
            end
        end)
    end
})

-------------------------------------------------------------------------------
-- last - Take last N items
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "last",
    description = "Take last N items",
    category = "query",
    acceptsInput = true,
    passthrough = true,  -- Preserves input type
    args = {
        { name = "count", required = false, description = "Number of items (default 1)" }
    },
    execute = function(ctx)
        local count = tonumber(ctx:GetArg(1)) or 1

        if not ctx:HasInput() then
            return ctx:Error("last requires pipeline input")
        end

        -- Need to collect to know what "last" means
        local items = C3.Pipeline.Collect(ctx:GetInput())
        local start = math.max(1, #items - count + 1)

        return ctx:Produce(function(emit)
            for i = start, #items do
                emit(items[i])
            end
        end)
    end
})

-------------------------------------------------------------------------------
-- random - Pick random items
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "random",
    description = "Pick random items from pipeline",
    category = "query",
    acceptsInput = true,
    passthrough = true,  -- Preserves input type
    args = {
        { name = "count", required = false, description = "Number of random items (default 1)" }
    },
    execute = function(ctx)
        local count = tonumber(ctx:GetArg(1)) or 1

        if not ctx:HasInput() then
            return ctx:Error("random requires pipeline input")
        end

        -- Must collect all items to pick randomly
        local items = C3.Pipeline.Collect(ctx:GetInput())

        if #items == 0 then
            return {}
        end

        -- Pick random items (without replacement if count > 1)
        if count == 1 then
            return { items[math.random(#items)] }
        end

        -- Fisher-Yates shuffle for multiple items
        local result = {}
        local indices = {}
        for i = 1, #items do
            indices[i] = i
        end

        for i = 1, math.min(count, #items) do
            local j = math.random(i, #items)
            indices[i], indices[j] = indices[j], indices[i]
            result[i] = items[indices[i]]
        end

        return result
    end
})

-------------------------------------------------------------------------------
-- take - Take N items
-- Uses short-circuit iteration - stops pulling from upstream after N items
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "take",
    description = "Take N items from pipeline",
    category = "query",
    acceptsInput = true,
    passthrough = true,  -- Preserves input type
    args = {
        { name = "count", required = true, description = "Number of items to take" }
    },
    execute = function(ctx)
        local count = tonumber(ctx:GetArg(1))
        if not count then
            return ctx:Error("take requires a number")
        end

        if not ctx:HasInput() then
            return ctx:Error("take requires pipeline input")
        end

        -- Short-circuit: stop pulling from upstream after we have enough
        local input = ctx:GetInput()
        local iter = type(input) == "function" and input or C3.Pipeline.Collect(input)

        if type(iter) == "table" then
            -- Input is array, just slice it
            local result = {}
            for i = 1, math.min(count, #iter) do
                result[i] = iter[i]
            end
            return result
        end

        -- Input is iterator - create short-circuit iterator
        return coroutine.wrap(function()
            local taken = 0
            for obj in iter do
                if taken >= count then
                    return  -- Stop pulling from upstream!
                end
                coroutine.yield(obj)
                taken = taken + 1
            end
        end)
    end
})

-------------------------------------------------------------------------------
-- skip - Skip N items
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "skip",
    description = "Skip first N items",
    category = "query",
    acceptsInput = true,
    passthrough = true,  -- Preserves input type
    args = {
        { name = "count", required = true, description = "Number of items to skip" }
    },
    execute = function(ctx)
        local count = tonumber(ctx:GetArg(1))
        if not count then
            return ctx:Error("skip requires a number")
        end

        if not ctx:HasInput() then
            return ctx:Error("skip requires pipeline input")
        end

        local skipped = 0
        return ctx:Transform(function(obj)
            if skipped < count then
                skipped = skipped + 1
                return nil
            end
            return obj
        end)
    end
})

-------------------------------------------------------------------------------
-- count - Count items
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "count",
    description = "Count items in pipeline",
    category = "query",
    acceptsInput = true,
    producesType = "Number",  -- Transforms to Number
    execute = function(ctx)
        if not ctx:HasInput() then
            return ctx:Error("count requires pipeline input")
        end

        local count = 0
        ctx:Consume(function(obj)
            count = count + 1
        end)

        return C3.Types.Create("Number", {
            value = count,
            display = tostring(count)
        })
    end
})

-------------------------------------------------------------------------------
-- select - Project specific fields
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "select",
    description = "Select specific fields from objects",
    category = "query",
    acceptsInput = true,
    args = {
        { name = "fields", required = true, description = "Field names (comma or space separated)",
          provider = function(partial, ctx)
              local beforeCursor = ctx and ctx.beforeCursor or ""

              -- Find what's after "select " - could have multiple fields already
              local afterSelect = beforeCursor:match("select%s+(.*)$") or ""

              -- Parse already-selected fields (split by comma or space)
              local selected = {}
              for field in afterSelect:gmatch("[^,%s]+") do
                  selected[field:lower()] = true
              end

              -- Get current partial (last token, might be incomplete)
              -- If ends with comma/space, partial is empty (ready for new field)
              local currentPartial = ""
              if not afterSelect:match("[,%s]$") then
                  currentPartial = afterSelect:match("([^,%s]+)$") or ""
                  -- Don't count the partial as already selected
                  selected[currentPartial:lower()] = nil
              end

              -- Collect all known field names from registered types
              local allFields = {}
              local seen = {}

              local typeNames = C3.Types:GetAll()
              for _, typeName in ipairs(typeNames) do
                  local typeDef = C3.Types:Get(typeName)
                  if typeDef.properties then
                      for _, prop in ipairs(typeDef.properties) do
                          -- Handle both array style {"name", "desc"} and hash style {name="name"}
                          local name = type(prop) == "table" and (prop.name or prop[1]) or prop
                          if name and type(name) == "string" and not seen[name] and not selected[name:lower()] then
                              seen[name] = true
                              allFields[#allFields + 1] = {
                                  text = name,
                                  description = typeName,
                              }
                          end
                      end
                  end
              end

              -- Filter by partial match
              local results = {}
              local partialLower = currentPartial:lower()
              for _, field in ipairs(allFields) do
                  if partialLower == "" or field.text:lower():find(partialLower, 1, true) then
                      results[#results + 1] = field
                  end
                  if #results >= C3.Constants.MAX_SUGGESTIONS then break end
              end

              return results
          end
        }
    },
    execute = function(ctx)
        -- Collect all args to support both "name,spellID" and "name spellID"
        local allArgs = ctx:GetArgs()
        local fieldsArg = table.concat(allArgs, " ")
        if not fieldsArg or fieldsArg == "" then
            return ctx:Error("Usage: select <field1> <field2> or select <field1,field2>")
        end

        -- Parse field list - split by comma or whitespace
        local fields = {}
        for field in fieldsArg:gmatch("[^,%s]+") do
            fields[#fields + 1] = field
        end

        if not ctx:HasInput() then
            return ctx:Error("select requires pipeline input")
        end

        return ctx:Transform(function(obj)
            if type(obj) ~= "table" then
                return obj
            end

            -- Always use Projection type to preserve rows even when values are nil
            local data = {}
            for _, field in ipairs(fields) do
                data[field] = obj[field]
            end

            local result = C3.Types.Create("Projection", data)
            result._meta.fields = fields  -- Store field order
            return result
        end)
    end
})

-------------------------------------------------------------------------------
-- unique - Remove duplicates
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "unique",
    description = "Remove duplicate items",
    category = "query",
    acceptsInput = true,
    passthrough = true,  -- Preserves input type
    args = {
        { name = "field", required = false, description = "Field to use for uniqueness" }
    },
    execute = function(ctx)
        local field = ctx:GetArg(1)

        if not ctx:HasInput() then
            return ctx:Error("unique requires pipeline input")
        end

        local seen = {}
        return ctx:Transform(function(obj)
            local key
            if field and type(obj) == "table" then
                key = tostring(obj[field])
            else
                key = tostring(obj)
            end

            if seen[key] then
                return nil
            end
            seen[key] = true
            return obj
        end)
    end
})

-------------------------------------------------------------------------------
-- grep - Filter objects by text match
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "grep",
    description = "Filter objects by text match",
    category = "query",
    acceptsInput = true,
    passthrough = true,  -- Preserves input type
    args = {
        { name = "pattern", required = true, description = "Text pattern to search for" }
    },
    flags = {
        v = { description = "Invert match (exclude matches)" },
        c = { description = "Case-sensitive match" },
        field = { description = "Search only this specific field" },
    },
    execute = function(ctx)
        local pattern = ctx:GetArg(1)
        if not pattern or pattern == "" then
            return ctx:Error("Usage: grep <pattern>")
        end

        if not ctx:HasInput() then
            return ctx:Error("grep requires pipeline input")
        end

        local invert = ctx:HasFlag("v")
        local caseSensitive = ctx:HasFlag("c")
        local specificField = ctx:GetFlag("field")
        local searchPattern = caseSensitive and pattern or pattern:lower()
        local patternLen = #pattern

        -- Get highlight color (bright red for grep matches)
        local highlightColor = "FF5555"

        local function matchesPattern(str)
            if not str or type(str) ~= "string" then
                return false
            end
            local target = caseSensitive and str or str:lower()
            return target:find(searchPattern, 1, true) ~= nil
        end

        -- Highlight all occurrences of pattern in a string (preserves original case)
        local function highlightMatches(str)
            if not str or type(str) ~= "string" then
                return str
            end
            local target = caseSensitive and str or str:lower()
            local result = {}
            local pos = 1
            local searchStart = 1

            while searchStart <= #str do
                local matchStart, matchEnd = target:find(searchPattern, searchStart, true)
                if not matchStart then
                    result[#result + 1] = str:sub(pos)
                    break
                end
                -- Add text before match
                if matchStart > pos then
                    result[#result + 1] = str:sub(pos, matchStart - 1)
                end
                -- Add highlighted match (using original case from str)
                local matchText = str:sub(matchStart, matchEnd)
                result[#result + 1] = string.format("|cFF%s%s|r", highlightColor, matchText)
                pos = matchEnd + 1
                searchStart = matchEnd + 1
            end

            return table.concat(result)
        end

        local function objectMatches(obj)
            if type(obj) ~= "table" then
                return matchesPattern(tostring(obj))
            end

            if specificField then
                local value = obj[specificField]
                if type(value) == "string" then
                    return matchesPattern(value)
                end
                return matchesPattern(tostring(value))
            end

            for key, value in pairs(obj) do
                if type(key) == "string" and key:sub(1, 1) ~= "_" then
                    if type(value) == "string" and matchesPattern(value) then
                        return true
                    end
                end
            end
            return false
        end

        -- Create a copy of obj with highlighted string fields
        local function highlightObject(obj)
            if type(obj) ~= "table" then
                return highlightMatches(tostring(obj))
            end

            local copy = {}
            for key, value in pairs(obj) do
                if type(key) == "string" and key:sub(1, 1) ~= "_" and type(value) == "string" then
                    if specificField then
                        -- Only highlight the specific field
                        if key == specificField then
                            copy[key] = highlightMatches(value)
                        else
                            copy[key] = value
                        end
                    else
                        -- Highlight all string fields that match
                        copy[key] = highlightMatches(value)
                    end
                else
                    copy[key] = value
                end
            end
            return copy
        end

        return ctx:Transform(function(obj)
            local matches = objectMatches(obj)
            if invert then
                -- Inverted: return non-matches without highlighting
                if not matches then return obj end
                return nil
            else
                -- Normal: return matches with highlighting
                if matches then return highlightObject(obj) end
                return nil
            end
        end)
    end
})

-------------------------------------------------------------------------------
-- group - Group by field
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "group",
    description = "Group items by field",
    category = "query",
    acceptsInput = true,
    producesType = "Group",  -- Transforms to Group
    args = {
        { name = "field", required = true, description = "Field to group by" }
    },
    execute = function(ctx)
        local field = ctx:GetArg(1)
        if not field then
            return ctx:Error("Usage: group <field>")
        end

        if not ctx:HasInput() then
            return ctx:Error("group requires pipeline input")
        end

        -- Collect and group
        local groups = {}
        local groupOrder = {}

        ctx:Consume(function(obj)
            local key = "nil"
            if type(obj) == "table" then
                key = tostring(obj[field] or "nil")
            end

            if not groups[key] then
                groups[key] = { key = key, items = {} }
                table.insert(groupOrder, key)
            end
            table.insert(groups[key].items, obj)
        end)

        -- Return groups
        return ctx:Produce(function(emit)
            for _, key in ipairs(groupOrder) do
                emit(C3.Types.Create("Group", {
                    key = key,
                    items = groups[key].items,
                    count = #groups[key].items,
                    display = string.format("%s (%d items)", key, #groups[key].items)
                }))
            end
        end)
    end
})

-------------------------------------------------------------------------------
-- sum - Sum numeric field
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "sum",
    description = "Sum numeric values",
    category = "query",
    acceptsInput = true,
    producesType = "Number",  -- Transforms to Number
    args = {
        { name = "field", required = false, description = "Field to sum (or use values directly)" }
    },
    execute = function(ctx)
        local field = ctx:GetArg(1)

        if not ctx:HasInput() then
            return ctx:Error("sum requires pipeline input")
        end

        local total = 0
        ctx:Consume(function(obj)
            total = total + (extractNumericValue(obj, field) or 0)
        end)

        return C3.Types.Create("Number", {
            value = total,
            display = tostring(total)
        })
    end
})

-------------------------------------------------------------------------------
-- avg - Average numeric field
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "avg",
    description = "Calculate average of numeric values",
    category = "query",
    acceptsInput = true,
    producesType = "Number",  -- Transforms to Number
    args = {
        { name = "field", required = false, description = "Field to average (or use values directly)" }
    },
    execute = function(ctx)
        local field = ctx:GetArg(1)

        if not ctx:HasInput() then
            return ctx:Error("avg requires pipeline input")
        end

        local total = 0
        local count = 0
        ctx:Consume(function(obj)
            local num = extractNumericValue(obj, field)
            if num then
                total = total + num
                count = count + 1
            end
        end)

        local avg = count > 0 and (total / count) or 0
        -- Round to 2 decimal places for display
        local display = string.format("%.2f", avg):gsub("%.?0+$", "")

        return C3.Types.Create("Number", {
            value = avg,
            display = display
        })
    end
})

-------------------------------------------------------------------------------
-- min - Minimum numeric field
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "min",
    description = "Find minimum numeric value",
    category = "query",
    acceptsInput = true,
    producesType = "Number",  -- Transforms to Number
    args = {
        { name = "field", required = false, description = "Field to find minimum (or use values directly)" }
    },
    execute = function(ctx)
        local field = ctx:GetArg(1)

        if not ctx:HasInput() then
            return ctx:Error("min requires pipeline input")
        end

        local minVal = nil
        ctx:Consume(function(obj)
            local num = extractNumericValue(obj, field)
            if num and (minVal == nil or num < minVal) then
                minVal = num
            end
        end)

        return C3.Types.Create("Number", {
            value = minVal or 0,
            display = tostring(minVal or 0)
        })
    end
})

-------------------------------------------------------------------------------
-- max - Maximum numeric field
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "max",
    description = "Find maximum numeric value",
    category = "query",
    acceptsInput = true,
    producesType = "Number",  -- Transforms to Number
    args = {
        { name = "field", required = false, description = "Field to find maximum (or use values directly)" }
    },
    execute = function(ctx)
        local field = ctx:GetArg(1)

        if not ctx:HasInput() then
            return ctx:Error("max requires pipeline input")
        end

        local maxVal = nil
        ctx:Consume(function(obj)
            local num = extractNumericValue(obj, field)
            if num and (maxVal == nil or num > maxVal) then
                maxVal = num
            end
        end)

        return C3.Types.Create("Number", {
            value = maxVal or 0,
            display = tostring(maxVal or 0)
        })
    end
})

-------------------------------------------------------------------------------
-- concat - Combine multiple query results
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "concat",
    description = "Combine results from multiple queries",
    category = "query",
    acceptsInput = true,
    passthrough = true,  -- Preserves input type (combines same-type items)
    args = {
        { name = "sources", vararg = true,
          description = "Multiple sources via $(query) subshells" }
    },
    execute = function(ctx)
        local allItems = {}

        -- If piped input exists, include it first
        if ctx:HasInput() then
            local input = ctx:GetInput()
            if type(input) == "function" then
                -- Iterator - collect it
                for obj in input do
                    allItems[#allItems + 1] = obj
                end
            elseif type(input) == "table" then
                if input._type then
                    -- Single typed object
                    allItems[#allItems + 1] = input
                else
                    -- Array of objects
                    for _, item in ipairs(input) do
                        allItems[#allItems + 1] = item
                    end
                end
            end
        end

        -- Add each subshell/argument result
        local args = ctx:GetArgs()
        for i = 1, #args do
            local result = ctx:GetArg(i)  -- Resolves subshells automatically
            if type(result) == "function" then
                for obj in result do
                    allItems[#allItems + 1] = obj
                end
            elseif type(result) == "table" then
                if result._type then
                    allItems[#allItems + 1] = result
                else
                    for _, item in ipairs(result) do
                        allItems[#allItems + 1] = item
                    end
                end
            end
        end

        if #allItems == 0 then
            return ctx:Error("concat: No input provided. Use $(query) subshells or pipe input.")
        end

        -- Return as lazy iterator
        return ctx:Produce(function(emit)
            for _, item in ipairs(allItems) do
                emit(item)
            end
        end)
    end
})

-------------------------------------------------------------------------------
-- Register Number and Group types
-------------------------------------------------------------------------------

C3.Types:Register("Number", {
    description = "Numeric result from aggregation operators",
    source = "count, sum, avg, min, max",
    properties = {
        { "value", "Numeric value" },
        { "display", "Formatted display string" },
    },
    format = function(self)
        return self.display or tostring(self.value)
    end
})

C3.Types:Register("Group", {
    description = "Grouped items from group command",
    source = "group",
    properties = {
        { "key", "Group key value" },
        { "items", "Array of items in group" },
        { "count", "Number of items" },
        { "display", "Formatted display string" },
    },
    format = function(self)
        return self.display or string.format("%s: %d items", self.key, self.count)
    end
})

C3.Types:Register("Projection", {
    description = "Field projection from select command",
    source = "select",
    properties = {
        { "fields", "Ordered array of field names (in _meta)" },
    },
    format = function(self)
        local parts = {}
        if self._meta and self._meta.fields then
            for _, field in ipairs(self._meta.fields) do
                local val = self[field]
                parts[#parts + 1] = val ~= nil and tostring(val) or ""
            end
        end
        return table.concat(parts, " | ")
    end
})

-------------------------------------------------------------------------------
-- types - Show available object types and their fields
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "types",
    description = "Show available object types and their fields",
    category = "help",
    args = {
        { name = "typename", required = false, description = "Type name to show details for",
          provider = function(partial, ctx)
              local beforeCursor = ctx and ctx.beforeCursor or ""

              -- Tokenize: split beforeCursor into words
              local tokens = {}
              for word in beforeCursor:gmatch("%S+") do
                  tokens[#tokens + 1] = word
              end

              -- Check if input ends with space (ready for next argument)
              local endsWithSpace = beforeCursor:match("%s$") ~= nil

              -- tokens[1] = "types", tokens[2] = typename, tokens[3] = fieldPartial
              local typeName = tokens[2]
              local fieldPartial = tokens[3] or ""

              -- If we have a valid type name AND (there's a 3rd token OR input ends with space after 2nd token)
              if typeName and C3.Types:Get(typeName) and (#tokens >= 3 or (#tokens == 2 and endsWithSpace)) then
                  -- Suggest fields for this type
                  local typeDef = C3.Types:Get(typeName)
                  if not typeDef.properties then
                      return {}
                  end

                  local suggestions = {}
                  local fieldPartialLower = fieldPartial:lower()
                  for _, prop in ipairs(typeDef.properties) do
                      local name = prop.name
                      local desc = prop.desc or ""
                      if fieldPartialLower == "" or name:lower():find(fieldPartialLower, 1, true) then
                          suggestions[#suggestions + 1] = {
                              text = typeName .. " " .. name,  -- Include typename for proper completion
                              description = desc,
                          }
                      end
                      if #suggestions >= C3.Constants.MAX_SUGGESTIONS then break end
                  end
                  return suggestions
              end

              -- Otherwise, suggest type names
              local suggestions = {}
              local typeNames = C3.Types:GetAll()
              local partialLower = (partial or ""):lower()
              for _, name in ipairs(typeNames) do
                  if partialLower == "" or name:lower():find(partialLower, 1, true) then
                      table.insert(suggestions, { text = name, display = name })
                  end
              end
              return suggestions
          end
        }
    },
    execute = function(ctx)
        local rawArg = ctx:GetArg(1)
        local results = {}

        -- Parse typename and optional fieldname from the argument
        -- Could be "Item" or "Item name"
        local typeName, fieldName
        if rawArg then
            typeName, fieldName = rawArg:match("^(%S+)%s+(%S+)$")
            if not typeName then
                typeName = rawArg:match("^(%S+)$")
            end
        end

        if typeName then
            -- Show specific type details
            local typeDef = C3.Types:Get(typeName)
            if not typeDef then
                return ctx:Error("Unknown type: " .. typeName)
            end

            if fieldName then
                -- Show specific field details with example usage
                local foundProp = nil
                local fieldLower = fieldName:lower()
                for _, prop in ipairs(typeDef.properties or {}) do
                    if prop.name and prop.name:lower() == fieldLower then
                        foundProp = prop
                        break
                    end
                end

                if not foundProp then
                    return ctx:Error("Unknown field '" .. fieldName .. "' on type " .. typeName)
                end

                -- Get source commands for examples (parse comma-separated list)
                local sourceCmds = {}
                if typeDef.source then
                    for src in typeDef.source:gmatch("([^,]+)") do
                        local cmd = src:match("^%s*(%S+)")
                        if cmd then
                            sourceCmds[#sourceCmds + 1] = cmd
                        end
                    end
                end
                if #sourceCmds == 0 then
                    sourceCmds = { "data" }
                end

                table.insert(results, C3.Types.Text(Colorize(typeName, "accent.primary") .. "." .. Colorize(foundProp.name, "accent.success")))
                if foundProp.desc then
                    table.insert(results, C3.Types.Text(Colorize(foundProp.desc, "status.dim")))
                end
                table.insert(results, C3.Types.Text(""))
                table.insert(results, C3.Types.Text(Colorize("Example usage:", "accent.warning")))

                -- Generate contextual examples based on field name patterns
                local fname = foundProp.name
                local isBoolean = fname:match("^is[A-Z]") or fname:match("^has[A-Z]")
                local isNumeric = fname == "ilvl" or fname == "level" or fname == "count" or fname == "quality" or fname == "amount" or fname:match("^max") or fname:match("^min")

                if isBoolean then
                    table.insert(results, C3.Types.Text("  " .. Colorize(sourceCmds[1] .. " | where " .. fname, "accent.info")))
                    local src2 = sourceCmds[2] or sourceCmds[1]
                    table.insert(results, C3.Types.Text("  " .. Colorize(src2 .. " | where not " .. fname, "accent.info")))
                elseif isNumeric then
                    table.insert(results, C3.Types.Text("  " .. Colorize(sourceCmds[1] .. " | where " .. fname .. " > 400", "accent.info")))
                    local src2 = sourceCmds[2] or sourceCmds[1]
                    table.insert(results, C3.Types.Text("  " .. Colorize(src2 .. " | sort " .. fname .. " desc", "accent.info")))
                    local src3 = sourceCmds[3] or sourceCmds[1]
                    table.insert(results, C3.Types.Text("  " .. Colorize(src3 .. " | sum " .. fname, "accent.info")))
                else
                    -- String field
                    table.insert(results, C3.Types.Text("  " .. Colorize(sourceCmds[1] .. " | where " .. fname .. " ~ \"text\"", "accent.info")))
                    local src2 = sourceCmds[2] or sourceCmds[1]
                    table.insert(results, C3.Types.Text("  " .. Colorize(src2 .. " | sort " .. fname, "accent.info")))
                    local src3 = sourceCmds[3] or sourceCmds[1]
                    table.insert(results, C3.Types.Text("  " .. Colorize(src3 .. " | group " .. fname, "accent.info")))
                end
            else
                -- Show all fields for type
                table.insert(results, C3.Types.Text(Colorize(typeName, "accent.primary")))
                if typeDef.description then
                    table.insert(results, C3.Types.Text(Colorize(typeDef.description, "status.dim")))
                end
                if typeDef.source then
                    table.insert(results, C3.Types.Text(Colorize("Source: ", "status.dim") .. Colorize(typeDef.source, "accent.info")))
                end
                table.insert(results, C3.Types.Text(""))
                table.insert(results, C3.Types.Text(Colorize("Fields:", "status.dim")))

                if typeDef.properties and #typeDef.properties > 0 then
                    for _, prop in ipairs(typeDef.properties) do
                        local name = type(prop) == "table" and prop.name or prop
                        local desc = type(prop) == "table" and prop.desc or nil
                        if desc then
                            table.insert(results, C3.Types.Text("  " .. Colorize(string.format("%-15s", name), "accent.success") .. " " .. Colorize(desc, "status.dim")))
                        else
                            table.insert(results, C3.Types.Text("  " .. Colorize(name, "accent.success")))
                        end
                    end
                else
                    table.insert(results, C3.Types.Text("  (no fields defined)"))
                end
            end
        else
            -- List all types
            table.insert(results, C3.Types.Text(Colorize("Available Types:", "accent.primary")))
            table.insert(results, C3.Types.Text(Colorize("Use 'types <name>' for field details", "status.dim")))
            table.insert(results, C3.Types.Text(""))

            local typeNames = C3.Types:GetAll()
            for _, name in ipairs(typeNames) do
                local typeDef = C3.Types:Get(name)
                local fieldCount = typeDef.properties and #typeDef.properties or 0
                local desc = typeDef.description and (" - " .. typeDef.description) or ""
                table.insert(results, C3.Types.Text("  " .. Colorize(string.format("%-15s", name), "accent.success") .. " " .. Colorize(string.format("(%d fields)%s", fieldCount, desc), "status.dim")))
            end
        end

        return results
    end
})

return true
