--[[
    Commander 3.0 - Command Registry

    Provides the C3.Command:Define() API for registering commands
    with metadata, argument validation, and auto-generated help.
]]

local ADDON_NAME, C3 = ...

-------------------------------------------------------------------------------
-- Command Definition Class
-------------------------------------------------------------------------------

local CommandDef = C3.class()
C3.CommandDef = CommandDef

---Create a command definition
---@param spec table
function CommandDef:init(spec)
    self.name = spec.name
    self.aliases = spec.aliases or {}
    self.description = spec.description or ""
    self.help = spec.help or ""
    self.category = spec.category or "misc"
    self.hidden = spec.hidden or false

    -- Flavor compatibility
    self.flavors = spec.flavors -- nil = all flavors

    -- Arguments specification
    self.args = spec.args or {}

    -- Handle declarative string providers (Negative Space pattern)
    -- Allows commands to use provider = "Mounts" instead of inline functions
    for _, arg in ipairs(self.args) do
        if type(arg.provider) == "string" then
            local providerName = arg.provider
            -- Create lazy wrapper that looks up provider at runtime
            arg.provider = function(partial, ctx)
                local handler = C3.Intellisense and C3.Intellisense:GetProvider(providerName)
                if handler then
                    return handler(partial, ctx)
                end
                return {}
            end
        end
    end

    -- Flags specification
    self.flags = spec.flags or {}

    -- The execute function
    self.execute = spec.execute

    -- Pipeline behavior
    self.acceptsInput = spec.acceptsInput ~= false  -- Default true
    self.producesOutput = spec.producesOutput ~= false  -- Default true

    -- Type propagation for intellisense
    self.producesType = spec.producesType  -- Output type name (e.g., "Item", "Mount")
    self.passthrough = spec.passthrough    -- true = preserves input type through pipeline
end

---Check if command is available in current flavor
---@return boolean
function CommandDef:IsAvailable()
    if not self.flavors then
        return true -- Available in all flavors
    end

    local currentFlavor = C3.Version:GetFlavor()
    for _, flavor in ipairs(self.flavors) do
        if flavor == currentFlavor then
            return true
        end
    end

    return false
end

---Validate arguments
---@param args table
---@return boolean valid
---@return string? error
function CommandDef:ValidateArgs(args)
    for i, argSpec in ipairs(self.args) do
        local arg = args[i]

        -- Check required
        if argSpec.required and (arg == nil or arg.value == nil) then
            return false, string.format("Missing required argument: %s", argSpec.name)
        end

        -- Skip validation if no value
        if arg == nil or arg.value == nil then
            break
        end

        -- Type validation
        if argSpec.type then
            local value = arg.value
            if argSpec.type == "number" then
                if type(value) ~= "number" and not tonumber(value) then
                    return false, string.format("Argument '%s' must be a number", argSpec.name)
                end
            elseif argSpec.type == "boolean" then
                if value ~= true and value ~= false and value ~= "true" and value ~= "false" then
                    return false, string.format("Argument '%s' must be a boolean", argSpec.name)
                end
            elseif argSpec.type == "string" then
                -- Non-empty string check (only if explicitly typed as string)
                if type(value) ~= "string" or value == "" then
                    return false, string.format("Argument '%s' must be a non-empty string", argSpec.name)
                end
            end
        end

        -- Custom validator
        if argSpec.validator then
            local valid, err = argSpec.validator(arg.value)
            if not valid then
                return false, err or string.format("Invalid value for '%s'", argSpec.name)
            end
        end

        -- Choices validation
        if argSpec.choices then
            local found = false
            for _, choice in ipairs(argSpec.choices) do
                if arg.value == choice then
                    found = true
                    break
                end
            end
            if not found then
                return false, string.format("'%s' must be one of: %s",
                    argSpec.name, table.concat(argSpec.choices, ", "))
            end
        end
    end

    return true
end

---Generate help text
---@return string
function CommandDef:GenerateHelp()
    local lines = {}

    -- Command name and description
    table.insert(lines, string.format("%s - %s", self.name, self.description))
    table.insert(lines, "")

    -- Usage
    local usage = self.name
    for _, arg in ipairs(self.args) do
        if arg.required then
            usage = usage .. " <" .. arg.name .. ">"
        else
            usage = usage .. " [" .. arg.name .. "]"
        end
    end
    table.insert(lines, "Usage: " .. usage)

    -- Arguments
    if #self.args > 0 then
        table.insert(lines, "")
        table.insert(lines, "Arguments:")
        for _, arg in ipairs(self.args) do
            local desc = arg.description or ""
            local req = arg.required and " (required)" or ""
            table.insert(lines, string.format("  %s%s - %s", arg.name, req, desc))
        end
    end

    -- Flags
    local flagList = {}
    for name, spec in pairs(self.flags) do
        table.insert(flagList, { name = name, spec = spec })
    end
    if #flagList > 0 then
        table.sort(flagList, function(a, b) return a.name < b.name end)
        table.insert(lines, "")
        table.insert(lines, "Flags:")
        for _, flag in ipairs(flagList) do
            local short = flag.spec.short and ("-" .. flag.spec.short .. ", ") or ""
            local desc = flag.spec.description or ""
            table.insert(lines, string.format("  %s--%s - %s", short, flag.name, desc))
        end
    end

    -- Detailed help
    if self.help and self.help ~= "" then
        table.insert(lines, "")
        table.insert(lines, self.help)
    end

    return table.concat(lines, "\n")
end

-------------------------------------------------------------------------------
-- Command Registry
-------------------------------------------------------------------------------

local Registry = {}
C3.Command = Registry

-- Storage
Registry._commands = {}    -- name -> CommandDef
Registry._aliases = {}     -- alias -> name
Registry._categories = {}  -- category -> { name, ... }

---Define a new command
---@param spec table Command specification
---@return CommandDef
function Registry:Define(spec)
    if not spec.name then
        error("Command must have a name")
    end
    if not spec.execute then
        error("Command must have an execute function")
    end

    local def = CommandDef:new(spec)

    -- Register main name
    self._commands[def.name] = def

    -- Register aliases
    for _, alias in ipairs(def.aliases) do
        self._aliases[alias] = def.name
    end

    -- Register category
    local cat = def.category
    if not self._categories[cat] then
        self._categories[cat] = {}
    end
    table.insert(self._categories[cat], def.name)

    -- Also register in the legacy C3.Commands table for executor compatibility
    C3.Commands[def.name] = function(ctx)
        return def.execute(ctx)
    end

    for _, alias in ipairs(def.aliases) do
        C3.Commands[alias] = C3.Commands[def.name]
    end

    return def
end

---Get command definition by name
---@param name string
---@return CommandDef?
function Registry:Get(name)
    -- Check direct name
    local def = self._commands[name]
    if def then return def end

    -- Check aliases
    local realName = self._aliases[name]
    if realName then
        return self._commands[realName]
    end

    return nil
end

---Check if command exists
---@param name string
---@return boolean
function Registry:Exists(name)
    return self:Get(name) ~= nil
end

---Get all commands
---@param includeHidden? boolean
---@return table
function Registry:GetAll(includeHidden)
    local result = {}
    for name, def in pairs(self._commands) do
        if def:IsAvailable() and (includeHidden or not def.hidden) then
            table.insert(result, def)
        end
    end
    table.sort(result, function(a, b) return a.name < b.name end)
    return result
end

---Get commands that accept pipeline input
---@param includeHidden boolean?
---@return table
function Registry:GetPipeable(includeHidden)
    local result = {}
    for name, def in pairs(self._commands) do
        if def:IsAvailable() and def.acceptsInput and (includeHidden or not def.hidden) then
            table.insert(result, def)
        end
    end
    table.sort(result, function(a, b) return a.name < b.name end)
    return result
end

---Get commands by category
---@param category string
---@return table
function Registry:GetByCategory(category)
    local names = self._categories[category] or {}
    local result = {}
    for _, name in ipairs(names) do
        local def = self._commands[name]
        if def and def:IsAvailable() then
            table.insert(result, def)
        end
    end
    return result
end

---Get all categories
---@return table
function Registry:GetCategories()
    local cats = {}
    for cat in pairs(self._categories) do
        table.insert(cats, cat)
    end
    table.sort(cats)
    return cats
end

---Execute a command by name
---@param name string
---@param ctx table Execution context
---@return any result
---@return string? error
function Registry:Execute(name, ctx)
    local def = self:Get(name)
    if not def then
        return nil, "Unknown command: " .. name
    end

    if not def:IsAvailable() then
        return nil, string.format("Command '%s' not available in %s",
            name, C3.Version:GetFlavor())
    end

    -- Validate arguments
    local valid, err = def:ValidateArgs(ctx.args or {})
    if not valid then
        return nil, err
    end

    -- Execute
    return def.execute(ctx)
end

---Generate autocomplete suggestions
---@param partial string
---@return table suggestions
function Registry:Autocomplete(partial)
    partial = partial:lower()
    local suggestions = {}

    for name, def in pairs(self._commands) do
        if def:IsAvailable() and not def.hidden then
            if name:lower():sub(1, #partial) == partial then
                table.insert(suggestions, {
                    text = name,
                    description = def.description,
                    category = def.category,
                })
            end
        end
    end

    -- Also check aliases
    for alias, realName in pairs(self._aliases) do
        local def = self._commands[realName]
        if def and def:IsAvailable() and not def.hidden then
            if alias:lower():sub(1, #partial) == partial then
                table.insert(suggestions, {
                    text = alias,
                    description = def.description .. " (alias)",
                    category = def.category,
                })
            end
        end
    end

    table.sort(suggestions, function(a, b) return a.text < b.text end)
    return suggestions
end

-------------------------------------------------------------------------------
-- Intellisense Provider for Commands
-------------------------------------------------------------------------------

-- Deferred registration - Intellisense module loads after registry
C3:OnInit(function()
    C3.Intellisense:RegisterProvider("Commands", function(partial, ctx)
        local results = {}
        local allCommands = Registry:GetAll(false)  -- exclude hidden

        for _, cmd in ipairs(allCommands) do
            if not cmd:IsAvailable() then
                -- skip unavailable commands
            elseif partial == "" or C3.StringUtils.fuzzyMatch(partial, cmd.name) then
                results[#results + 1] = {
                    text = cmd.name,
                    description = cmd.description or "No description",
                    category = cmd.category,
                }
            end
            if #results >= C3.Constants.MAX_SUGGESTIONS then break end
        end
        return results
    end)
end)

-------------------------------------------------------------------------------
-- Built-in Commands
-------------------------------------------------------------------------------

---echo - Print arguments
Registry:Define({
    name = "echo",
    description = "Print arguments",
    category = "system",
    acceptsInput = false,
    args = {
        { name = "text", vararg = true, description = "Text to print" },
    },
    execute = function(ctx)
        local args = ctx:GetArgs()
        if #args == 0 then
            return ctx:Text("")
        end

        -- Join all arguments with spaces
        local parts = {}
        for _, arg in ipairs(args) do
            if type(arg) == "table" then
                -- If it's an object, format it
                if arg._type then
                    table.insert(parts, C3.Formatter:Format(arg))
                else
                    table.insert(parts, tostring(arg))
                end
            else
                table.insert(parts, tostring(arg))
            end
        end

        return ctx:Text(table.concat(parts, " "))
    end
})

---help - Show help
Registry:Define({
    name = "help",
    description = "Show help for commands",
    category = "system",
    acceptsInput = false,
    args = {
        { name = "command", required = false, description = "Command to get help for", provider = "Commands" },
    },
    execute = function(ctx)
        local cmdName = ctx:GetArg(1)

        if cmdName then
            -- Show help for specific command
            local def = Registry:Get(cmdName)
            if not def then
                return ctx:Error("Unknown command: " .. cmdName)
            end
            return ctx:Text(def:GenerateHelp())
        end

        -- List all commands
        local lines = {}
        for _, def in ipairs(Registry:GetAll()) do
            table.insert(lines, string.format("|cFF00FFFF%s|r - %s", def.name, def.description or ""))
        end
        return ctx:Text(table.concat(lines, "\n"))
    end
})

---clear - Clear the output
Registry:Define({
    name = "clear",
    description = "Clear the terminal output",
    category = "system",
    acceptsInput = false,
    execute = function(ctx)
        if ctx.session and ctx.session._activeBuffer then
            ctx.session._activeBuffer:Clear()
        end
        return nil -- No output
    end
})

---operators - List all pipeline operators
Registry:Define({
    name = "operators",
    description = "List all pipeline operators",
    category = "system",
    acceptsInput = false,
    execute = function(ctx)
        local lines = {
            "|cFFEBCB8BPipeline Operators|r",  -- nord13 yellow
            "Use these after | to transform data:",
            "",
            "|cFF88C0D0where|r <expr>     - Filter by expression",  -- nord8 cyan
            "                     bags | where quality > 3",
            "                     quests | where isComplete",
            "",
            "|cFF88C0D0sort|r <field>     - Sort by field (add 'desc' for descending)",
            "                     bags | sort ilvl desc",
            "                     mounts | sort name",
            "",
            "|cFF88C0D0select|r <fields>  - Select specific fields",
            "                     bags | select name, ilvl, quality",
            "",
            "|cFF88C0D0take|r <n>         - Take first N items",
            "                     bags | take 5",
            "",
            "|cFF88C0D0skip|r <n>         - Skip first N items",
            "                     bags | skip 10 | take 5",
            "",
            "|cFF88C0D0first|r            - Get first item only",
            "|cFF88C0D0last|r             - Get last item only",
            "",
            "|cFF88C0D0count|r            - Count items",
            "                     bags | where quality > 2 | count",
            "",
            "|cFF88C0D0sum|r <field>      - Sum numeric field",
            "                     currencies | sum quantity",
            "",
            "|cFF88C0D0group|r <field>    - Group by field",
            "                     bags | group type",
            "",
            "|cFF88C0D0unique|r <field>   - Remove duplicates by field",
            "                     spells | unique school",
            "",
            "|cFF88C0D0concat|r           - Combine multiple sources",
            "                     concat $(bags) $(equipped)",
            "",
            "|cFF88C0D0types|r [name]     - Show type fields",
            "                     types Item",
        }
        return ctx:Text(table.concat(lines, "\n"))
    end
})

return Registry
