--[[-- Core Module - Commands
- Factorio command making module that makes commands with better parse and more modularity

--- Adding a permission authority
-- You are only required to return a boolean, but by using the unauthorised status you can provide better feedback to the user
Commands.add_permission_authority(function(player, command)
    if command.flags.admin_only and not player.admin then
        return Commands.status.unauthorised("This command requires in-game admin")
    end
    return Commands.status.success()
end)

--- Adding a data type
-- You can not return nil from this function, doing so will raise an error, you must return a status
Commands.add_data_type("integer", function(input, player)
    local number = tonumber(input)
    if number == nil then
        return Commands.status.invalid_input("Value must be a valid number")
    else
        return Commands.status.success(number)
    end
end)

-- It is recommend to use exiting parsers within your own to simplify checks, but make sure to propagate failures
Commands.add_data_type("integer-range", function(input, player, minimum, maximum)
    local success, status, integer = Commands.parse_data_type("integer", input, player)
    if not success then return status, number end

    if integer < minimum or integer > maximum then
        return Commands.status.invalid_input(string.format("Integer must be in range: %d to %d", minimum, maximum))
    else
        return Commands.status.success(integer)
    end
end)

--- Adding a command
Commands.new("repeat", "This is my new command, it will repeat a message a number of times")
:add_flags{ "admin_only" } -- Using the permission authority above, this makes the command admin only
:add_aliases{ "repeat-message" } -- You can add as many aliases as you want
:enable_auto_concatenation() -- This allows the final argument to be any length
:argument("count", "integer-range", 1, 10) -- Allow any value between 1 and 10
:optional("message", "string") -- This is an optional argument
:defaults{
    -- Defaults don't need to be functions, one is used here to demonstrate their use, remember player can be nil for the server
    message = function(player)
        return player and "Hello, "..player.name or "Hello, World!"
    end
}
:register(function(player, count, message)
    for i = 1, count do
        Commands.print("#"..i.." "..message)
    end
end)

]]

local ExpUtil = require("modules/exp_util")
local Search = require("modules/exp_commands/search")

--- Used as "game.player" in Commands.print when "game.player" is nil
--- @type LuaPlayer?
local _print_player

--- @class Commands
local Commands = {
    color = ExpUtil.color,
    format_rich_text_color = ExpUtil.format_rich_text_color,
    format_rich_text_color_locale = ExpUtil.format_rich_text_color_locale,
    format_player_name = ExpUtil.format_player_name,
    format_player_name_locale = ExpUtil.format_player_name_locale,

    registered_commands = {}, --- @type table<string, ExpCommand> Stores a reference to all registered commands
    permission_authorities = {}, --- @type Commands.PermissionAuthority[] Stores a reference to all active permission authorities

    --- @package Stores the event handlers
    events = {
        [defines.events.on_player_locale_changed] = Search.on_player_locale_changed,
        [defines.events.on_player_joined_game] = Search.on_player_locale_changed,
        [defines.events.on_string_translated] = Search.on_string_translated,
    },
}

--- @class Commands.status: table<string, Commands.Status>
--- Contains the different status values a command can return
Commands.status = {}

--- @class Commands.types: table<string, Commands.InputParser | Commands.InputParserFactory>
--- Stores all input parsers and validators for different data types
Commands.types = {}

--- @package
function Commands.on_init() Search.prepare(Commands.registered_commands) end

--- @package
function Commands.on_load() Search.prepare(Commands.registered_commands) end

--- @alias Commands.Callback fun(player: LuaPlayer, ...: any?): Commands.Status?, LocalisedString?
--- This is a default callback that should never be called
local function default_command_callback()
    return Commands.status.internal_error("No callback registered")
end

--- @class Commands.Argument
--- @field name string The name of the argument
--- @field description LocalisedString? The description of the argument
--- @field input_parser Commands.InputParser The input parser for the argument
--- @field optional boolean True when the argument is optional
--- @field default any? The default value of the argument

--- @class Commands.Command
--- @field name string The name of the command
--- @field description LocalisedString The description of the command
--- @field usage LocalisedString The usage of the command
--- @field help_text LocalisedString The full help text for the command
--- @field aliases string[] Aliases that the command will also be registered under
--- @field defined_at? string If present then this is an ExpCommand

--- @class ExpCommand: Commands.Command
--- @field callback Commands.Callback The callback which is ran for the command
--- @field defined_at string The file location that the command is defined at
--- @field auto_concat boolean True if the command auto concatenates tailing parameters into a single string 
--- @field min_arg_count number The minimum number of expected arguments
--- @field max_arg_count number The maximum number of expected arguments
--- @field flags table Stores flags which can be used by permission authorities
--- @field arguments Commands.Argument[] The arguments for this command
--- @overload fun(player: LuaPlayer, ...: any)
Commands._prototype = {}

Commands._metatable = {
    __index = Commands._prototype,
    __class = "ExpCommand",
}

Commands.server = setmetatable({
    index = 0,
    color = ExpUtil.color.white,
    chat_color = ExpUtil.color.white,
    name = "<server>",
    locale = "en",
    tag = "",
    connected = true,
    admin = true,
    afk_time = 0,
    online_time = 0,
    last_online = 0,
    spectator = true,
    show_on_map = false,
    valid = true,
    object_name = "LuaPlayer",
    print = rcon.print,
}, {
    -- To prevent unnecessary logging Commands.error is called here and error is filtered by command_callback
    __index = function(_, key)
        if key == "__self" or type(key) == "number" then return nil end
        Commands.error("Command does not support rcon usage, requires LuaPlayer." .. key)
        error("Command does not support rcon usage, requires LuaPlayer." .. key)
    end,
    __newindex = function(_, key)
        Commands.error("Command does not support rcon usage, requires LuaPlayer." .. key)
        error("Command does not support rcon usage, requires LuaPlayer." .. key)
    end,
}) --[[ @as LuaPlayer ]]

--- Status Returns.
-- Return values used by command callbacks

--- @alias Commands.Status fun(msg: LocalisedString?): Commands.Status, LocalisedString

--- Used to signal success from a command, data type parser, or permission authority
--- @param msg LocalisedString? An optional message to be included when a command completes (only has an effect in command callbacks)
--- @return Commands.Status, LocalisedString # Should be returned directly without modification
function Commands.status.success(msg)
    return Commands.status.success, msg == nil and { "exp-commands.success" } or msg
end

--- Used to signal an error has occurred in a command, data type parser, or permission authority
--- For data type parsers and permission authority, an error return will prevent the command from being executed
--- @param msg LocalisedString? An optional error message to be included in the output, a generic message is used if not provided
--- @return Commands.Status, LocalisedString # Should be returned directly without modification
function Commands.status.error(msg)
    return Commands.status.error, { "exp-commands.error", msg == nil and { "exp-commands.error-default" } or msg }
end

--- Used to signal the player is unauthorised to use a command, primarily used by permission authorities but can be used in a command callback
--- For permission authorities, an unauthorised return will prevent the command from being executed
--- @param msg LocalisedString? An optional error message to be included in the output, a generic message is used if not provided
--- @return Commands.Status, LocalisedString # Should be returned directly without modification
function Commands.status.unauthorised(msg)
    return Commands.status.unauthorised, { "exp-commands.unauthorized", msg == nil and { "exp-commands.unauthorized-default" } or msg }
end

--- Used to signal the player provided invalid input to an command, primarily used by data type parsers but can be used in a command callback
--- For data type parsers, an invalid_input return will prevent the command from being executed
--- @param msg LocalisedString? An optional error message to be included in the output, a generic message is used if not provided
--- @return Commands.Status, LocalisedString # Should be returned directly without modification
function Commands.status.invalid_input(msg)
    return Commands.status.invalid_input, msg == nil and { "exp-commands.invalid-input" } or msg
end

--- Used to signal an internal error has occurred, this is reserved for internal use only
--- @param msg LocalisedString A message detailing the error which has occurred, will be logged and outputted
--- @return Commands.Status, LocalisedString # Should be returned directly without modification
--- @package
function Commands.status.internal_error(msg)
    return Commands.status.internal_error, { "exp-commands.internal-error", msg }
end

--- @type table<Commands.Status, string>
local valid_command_status = {} -- Hashmap lookup for testing if a status is valid
for name, status in pairs(Commands.status) do
    valid_command_status[status] = name
end

--- Permission Authority.
-- Functions that control who can use commands

--- @alias Commands.PermissionAuthority fun(player: LuaPlayer, command: ExpCommand): boolean|Commands.Status, LocalisedString?

--- Add a permission authority, a permission authority is a function which provides access control for commands, multiple can be active at once
--- When multiple are active, all authorities must give permission for the command to execute, if any deny access then the command is not ran
--- @param permission_authority Commands.PermissionAuthority The function to provide access control to commands, see module usage.
--- @return Commands.PermissionAuthority # The function which was provided as the first argument
function Commands.add_permission_authority(permission_authority)
    for _, value in ipairs(Commands.permission_authorities) do
        if value == permission_authority then
            return permission_authority
        end
    end

    local next_index = #Commands.permission_authorities + 1
    Commands.permission_authorities[next_index] = permission_authority
    return permission_authority
end

--- Remove a permission authority, must be the same function reference which was passed to add_permission_authority
--- @param permission_authority Commands.PermissionAuthority The access control function to remove as a permission authority
function Commands.remove_permission_authority(permission_authority)
    local pas = Commands.permission_authorities
    for index, value in ipairs(pas) do
        if value == permission_authority then
            local last = #pas
            pas[index] = pas[last]
            pas[last] = nil
            return
        end
    end
end

--- Check if a player has permission to use a command, calling all permission authorities
--- @param player LuaPlayer? The player to test the permission of, nil represents the server and always returns true
--- @param command ExpCommand The command the player is attempting to use
--- @return boolean # True if the player has permission to use the command
--- @return LocalisedString? # When permission is denied, this is the reason permission was denied
function Commands.player_has_permission(player, command)
    if player == nil or player == Commands.server then return true end

    for _, permission_authority in ipairs(Commands.permission_authorities) do
        local status, msg = permission_authority(player, command)
        if type(status) == "boolean" then
            if status == false then
                local _, rtn_msg = Commands.status.unauthorised(msg)
                return false, rtn_msg
            end
        elseif status and valid_command_status[status] then
            if status ~= Commands.status.success then
                return false, msg
            end
        else
            error("Permission authority returned unexpected value: " .. ExpUtil.get_class_name(status))
        end
    end

    return true, nil
end

--- Data Type Parsing.
-- Functions that parse and validate player input

--- @alias Commands.InputParser<T> fun(input: string, player: LuaPlayer): Commands.Status, (T | LocalisedString)

--- @alias Commands.InputParserFactory<T> fun(...: any): Commands.InputParser<T>

--- Add a new input parser to the command library, this method validates that it does not already exist
--- @generic T : Commands.InputParser | Commands.InputParserFactory
--- @param data_type string The name of the data type the input parser reads in and validates, becomes a key of Commands.types
--- @param input_parser T The function used to parse and validate the data type
--- @return T # The function which was provided as the second argument
function Commands.add_data_type(data_type, input_parser)
    if Commands.types[data_type] then
        local defined_at = ExpUtil.get_function_name(Commands.types[data_type], true)
        error("Data type \"" .. tostring(data_type) .. "\" already has a parser registered: " .. defined_at, 2)
    end
    Commands.types[data_type] = input_parser
    return input_parser
end

--- Remove an input parser for a data type, must be the same string that was passed to add_input_parser
--- @param data_type string | Commands.InputParser | Commands.InputParserFactory The data type or input parser you want to remove the input parser for
function Commands.remove_data_type(data_type)
    Commands.types[data_type] = nil
    for k, v in pairs(Commands.types) do
        if v == data_type then
            Commands.types[k] = nil
            return
        end
    end
end

--- Parse and validate an input string as a given data type
--- @generic T
--- @param input string The input string
--- @param player LuaPlayer The player who gave the input
--- @param input_parser Commands.InputParser<T> The parser to apply to the input string
--- @return boolean success True when the input was successfully parsed and validated to be the correct type
--- @return Commands.Status status, T | LocalisedString result # If success is false then Remaining values should be returned directly without modification
function Commands.parse_input(input, player, input_parser)
    local status, status_msg = input_parser(input, player)
    if status == nil or not valid_command_status[status] then
        local data_type = table.get_key(Commands.types, input_parser) or ExpUtil.get_function_name(input_parser, true)
        error("Parser for data type \"" .. data_type .. "\" did not return a valid status got: " .. ExpUtil.get_class_name(status))
    end
    return status == Commands.status.success, status, status_msg
end

--- List and Search
-- Functions used to list and search for commands

--- Returns a list of all registered custom commands
--- @return table<string,ExpCommand> # A dictionary of commands
function Commands.list_all()
    return Commands.registered_commands
end

--- Returns a list of all registered custom commands which the given player has permission to use
--- @param player LuaPlayer? The player to get the command of, nil represents the server but list_all should be used
--- @return table<string,ExpCommand>  # A dictionary of commands
function Commands.list_for_player(player)
    local rtn = {}

    for name, command in pairs(Commands.registered_commands) do
        if Commands.player_has_permission(player, command) then
            rtn[name] = command
        end
    end

    return rtn
end

--- Searches all custom commands and game commands for the given keyword
--- @param keyword string The keyword to search for
--- @return table<string,Commands.Command>  # A dictionary of commands
function Commands.search_all(keyword)
    return Search.search_commands(keyword, Commands.list_all(), "en")
end

--- Searches custom commands allowed for this player and all game commands for the given keyword
--- @param keyword string The keyword to search for
--- @param player LuaPlayer? The player to search the commands of, nil represents server but search_all should be used
--- @return table<string,Commands.Command>  # A dictionary of commands
function Commands.search_for_player(keyword, player)
    return Search.search_commands(keyword, Commands.list_for_player(player), player and player.locale)
end

--- Command Output
-- Prints output to the player or rcon connection

local print_format_options = { max_line_count = 20 }
local print_settings_default = { sound_path = "utility/scenario_message", color = ExpUtil.color.white }
local print_settings_error = { sound_path = "utility/wire_pickup", color = ExpUtil.color.orange_red }

Commands.print_settings = {
    default = print_settings_default,
    error = print_settings_error,
}

--- Print a message to the user of a command, accepts any value and will print in a readable and safe format
--- @param message any The message / value to be printed
--- @param settings PrintSettings? The settings to print with
function Commands.print(message, settings)
    local player = game.player or _print_player
    if not player then
        rcon.print(ExpUtil.format_any(message))
    else
        if not settings then
            settings = print_settings_default
        elseif not settings.sound_path then
            settings.sound_path = print_settings_default.sound_path
        end
        player.print(ExpUtil.format_any(message, print_format_options), settings)
    end
end

--- Print an error message to the user of a command, accepts any value and will print in a readable and safe format
--- @param message any The message / value to be printed
function Commands.error(message)
    Commands.print(message, print_settings_error)
end

--- Command Prototype
-- The prototype definition for command objects

local function assert_command_mutable(command)
    if Commands.registered_commands[command.name] then
        error("Command cannot be modified after being registered.", 3)
    end
end

--- Returns a new command object, this will not register the command but act as a way to start construction
--- @param name string The name of the command as it will be registered later
--- @param description LocalisedString? The description of the command displayed in the help message
--- @return ExpCommand
function Commands.new(name, description)
    ExpUtil.assert_argument_type(name, "string", 1, "name")
    if Commands.registered_commands[name] then
        error("Command is already defined at: " .. Commands.registered_commands[name].defined_at, 2)
    end

    return setmetatable({
        name = name,
        description = description or "",
        help_text = description, -- Will be replaced in command:register
        callback = default_command_callback, -- Will be replaced in command:register
        defined_at = ExpUtil.safe_file_path(2),
        auto_concat = false,
        min_arg_count = 0,
        max_arg_count = 0,
        flags = {},
        aliases = {},
        arguments = {},
    }, Commands._metatable)
end

--- Add a new required argument to the command of the given data type
--- @param name string The name of the argument being added
--- @param description LocalisedString? The description of the argument being added
--- @param input_parser Commands.InputParser The input parser to be used for the argument
--- @return ExpCommand
function Commands._prototype:argument(name, description, input_parser)
    assert_command_mutable(self)
    if self.min_arg_count ~= self.max_arg_count then
        error("Can not have required arguments after optional arguments", 2)
    end
    self.min_arg_count = self.min_arg_count + 1
    self.max_arg_count = self.max_arg_count + 1
    self.arguments[#self.arguments + 1] = {
        name = name,
        description = description,
        input_parser = input_parser,
        optional = false,
    }
    return self
end

--- Add a new optional argument to the command of the given data type
--- @param name string The name of the argument being added
--- @param description LocalisedString? The description of the argument being added
--- @param input_parser Commands.InputParser The input parser to be used for the argument
--- @return ExpCommand
function Commands._prototype:optional(name, description, input_parser)
    assert_command_mutable(self)
    self.max_arg_count = self.max_arg_count + 1
    self.arguments[#self.arguments + 1] = {
        name = name,
        description = description,
        input_parser = input_parser,
        optional = true,
    }
    return self
end

--- Set the defaults for optional arguments, any not provided will have their value as nil
--- @param defaults table<string, (fun(player: LuaPlayer): any) | any> The default values for the optional arguments, the key is the name of the argument
--- @return ExpCommand
function Commands._prototype:defaults(defaults)
    assert_command_mutable(self)
    local matched = {}
    for _, argument in ipairs(self.arguments) do
        if defaults[argument.name] then
            if not argument.optional then
                error("Attempting to set default value for required argument: " .. argument.name, 2)
            end
            argument.default = defaults[argument.name]
            matched[argument.name] = true
        end
    end

    -- Check that there are no extra values in the table
    for name in pairs(defaults) do
        if not matched[name] then
            error("No argument with name: " .. name, 2)
        end
    end

    return self
end

--- Set the flags for the command, these can be accessed by permission authorities to check who can use a command
--- @param flags table An array of strings or a dictionary of flag names and values, when an array is used the flags values are set to true
--- @return ExpCommand
function Commands._prototype:add_flags(flags)
    assert_command_mutable(self)
    for name, value in pairs(flags) do
        if type(name) == "number" then
            self.flags[value] = true
        else
            self.flags[name] = value
        end
    end

    return self
end

--- Set the aliases for the command, these are alternative names that the command can be ran under
--- @param aliases string[] An array of string names to use as aliases to this command
--- @return ExpCommand
function Commands._prototype:add_aliases(aliases)
    assert_command_mutable(self)
    local start_index = #self.aliases
    for index, alias in ipairs(aliases) do
        self.aliases[start_index + index] = alias
    end

    return self
end

--- Enable concatenation of all arguments after the last, this should be used for user provided reason text
--- @return ExpCommand
function Commands._prototype:enable_auto_concatenation()
    assert_command_mutable(self)
    self.auto_concat = true
    return self
end

--- Register the command to the game with the given callback, this must be the final step as the object becomes immutable afterwards
--- @param callback Commands.Callback The function which is called to perform the command action
--- @return ExpCommand
function Commands._prototype:register(callback)
    assert_command_mutable(self)
    Commands.registered_commands[self.name] = self
    self.callback = callback

    -- Generates a description to be used
    local argument_names = { "" } --- @type LocalisedString
    local argument_verbose = { "" } --- @type LocalisedString
    self.usage = { "exp-commands.usage", self.name, argument_names }
    self.help_text = { "exp-commands.help", argument_names, self.description, argument_verbose } --- @type LocalisedString
    if next(self.aliases) then
        argument_verbose[2] = { "exp-commands.aliases", table.concat(self.aliases, ", ") }
    end

    local verbose_index = #argument_verbose
    for index, argument in pairs(self.arguments) do
        if argument.optional then
            argument_names[index + 1] = { "exp-commands.optional", argument.name }
            if argument.description and argument.description ~= "" then
                verbose_index = verbose_index + 1
                argument_verbose[verbose_index] = { "exp-commands.optional-verbose", argument.name, argument.description }
            end
        else
            argument_names[index + 1] = { "exp-commands.argument", argument.name }
            if argument.description and argument.description ~= "" then
                verbose_index = verbose_index + 1
                argument_verbose[verbose_index] = { "exp-commands.argument-verbose", argument.name, argument.description }
            end
        end
    end

    -- Callback which is called by the game engine
    ---@param event CustomCommandData
    local function command_callback(event)
        event.name = self.name
        local success, traceback = xpcall(Commands._event_handler, debug.traceback, event)
        --- @cast traceback string
        if not success and not traceback:find("Command does not support rcon usage") then
            local key = "<" .. table.concat({ event.name, event.player_index or 0, event.tick }, ":") .. ">"
            local _, msg = Commands.status.internal_error(key)
            Commands.error(msg)
            log("Internal Command Error " .. key .. "\n" .. traceback)
        end
    end

    -- Registers the command under its own name
    commands.add_command(self.name, self.help_text, command_callback)

    -- Registers the command under its aliases
    for _, alias in ipairs(self.aliases) do
        commands.add_command(alias, self.help_text, command_callback)
    end

    return self
end

--- Run a callback for a command in the same way the command processor would, note no type validation is performed
--- @param self ExpCommand
--- @param player LuaPlayer
--- @param ... any
function Commands._metatable.__call(self, player, ...)
    _print_player = player
    local status, status_msg = self.callback(player, ...)
    if status == nil then
        local _, msg = Commands.status.success()
        Commands.print(msg)
    elseif not valid_command_status[status] then
        error("Command \"" .. self.name .. "\" did not return a valid status got: " .. ExpUtil.get_class_name(status))
    elseif status ~= Commands.status.success then
        Commands.error(status_msg)
    else
        Commands.print(status_msg)
    end
    _print_player = nil
end

--- Command Runner
-- Used internally to run commands

--- Log that a command was attempted and its outcome (error / success)
--- @param comment string The main comment to include in the log
--- @param command ExpCommand The command that is being executed
--- @param player LuaPlayer The player who is running the command
--- @param parameter string The raw command parameter that was used 
--- @param detail any
local function log_command(comment, command, player, parameter, detail)
    if player.index == 0 and comment == "Command Ran" then return end
    ExpUtil.write_json("log/commands.log", {
        comment = comment,
        command_name = command.name,
        player_name = player.name,
        parameter = parameter,
        detail = detail,
    })
end

--- Extract the arguments from a string input string
--- @param raw_input string? The raw input from the player
--- @param max_args number The maximum number of allowed arguments
--- @param auto_concat boolean True when remaining arguments should be concatenated
--- @return table? # Nil if there are too many arguments
local function extract_arguments(raw_input, max_args, auto_concat)
    -- nil check when no input given
    if raw_input == nil then return {} end

    -- Extract quoted arguments
    local quoted_arguments = {}
    local input_string = raw_input:gsub('"[^"]-"', function(word)
        local no_spaces = word:gsub("%s", "%%s")
        quoted_arguments[no_spaces] = word:sub(2, -2)
        return " " .. no_spaces .. " "
    end)

    -- Extract all arguments
    local index = 0
    local arguments = {}
    for word in input_string:gmatch("%S+") do
        index = index + 1
        if index > max_args then
            -- concat the word onto the last argument
            if auto_concat == false then
                return nil -- too many args, exit early
            elseif quoted_arguments[word] then
                arguments[max_args] = arguments[max_args] .. ' "' .. quoted_arguments[word] .. '"'
            else
                arguments[max_args] = arguments[max_args] .. " " .. word
            end
        else
            -- new argument to be added
            if quoted_arguments[word] then
                arguments[index] = quoted_arguments[word]
            else
                arguments[index] = word
            end
        end
    end

    return arguments
end

--- Internal event handler for the command event
--- @param event CustomCommandData
--- @return nil
function Commands._event_handler(event)
    local command = Commands.registered_commands[event.name]
    if command == nil then
        error("Command not recognised: " .. event.name)
    end

    local player = Commands.server
    if event.player_index then
        player = game.players[event.player_index]
    end

    -- Check if the player is allowed to use the command
    local allowed, failure_msg = Commands.player_has_permission(player, command)
    if not allowed then
        log_command("Command not allowed", command, player, event.parameter)
        return Commands.error(failure_msg)
    end

    -- Check the edge case of parameter being nil
    if command.min_arg_count > 0 and event.parameter == nil then
        log_command("Too few arguments", command, player, event.parameter, { minimum = command.min_arg_count, maximum = command.max_arg_count })
        return Commands.error{ "exp-commands.invalid-usage", command.usage }
    end

    -- Get the arguments for the command, returns nil if there are too many arguments
    local raw_arguments = extract_arguments(event.parameter, command.max_arg_count, command.auto_concat)
    if raw_arguments == nil then
        log_command("Too many arguments", command, player, event.parameter, { minimum = command.min_arg_count, maximum = command.max_arg_count })
        return Commands.error{ "exp-commands.invalid-usage", command.usage }
    end

    -- Check the minimum number of arguments is fulfilled
    if #raw_arguments < command.min_arg_count then
        log_command("Too few arguments", command, player, event.parameter, { minimum = command.min_arg_count, maximum = command.max_arg_count })
        return Commands.error{ "exp-commands.invalid-usage", command.usage }
    end

    -- Parse the arguments, optional arguments will attempt to use a default if provided
    local arguments = {}
    for index, argument in ipairs(command.arguments) do
        local input = raw_arguments[index]
        if input == nil then
            -- We know this is an optional argument because the minimum count is satisfied
            assert(argument.optional == true, "Argument was required")
            if type(argument.default) == "function" then
                arguments[index] = argument.default(player)
            else
                arguments[index] = argument.default
            end
        else
            -- Parse the raw argument to get the correct data type
            local success, status, parsed = Commands.parse_input(input, player, argument.input_parser)
            if success == false then
                log_command("Input parse failed", command, player, event.parameter, { status = valid_command_status[status], index = index, argument = argument.name, reason = parsed })
                return Commands.error{ "exp-commands.invalid-argument", argument.name, parsed }
            else
                arguments[index] = parsed
            end
        end
    end

    -- Run the command, don't need xpcall here because errors are caught in command_callback
    local status, status_msg = command.callback(player, table.unpack(arguments))
    if status == nil then
        log_command("Command Ran", command, player, event.parameter)
        local _, msg = Commands.status.success()
        return Commands.print(msg)
    elseif not valid_command_status[status] then
        error("Command \"" .. command.name .. "\" did not return a valid status got: " .. ExpUtil.get_class_name(status))
    elseif status ~= Commands.status.success then
        log_command("Custom Error", command, player, event.parameter, status_msg)
        return Commands.error(status_msg)
    else
        log_command("Command Ran", command, player, event.parameter)
        return Commands.print(status_msg)
    end
end

return Commands
