---@diagnostic disable: deprecated
--luacheck: ignore 561
local Global = require 'utils.global'
local Core = require 'utils.core'
local Session = require 'utils.datastore.session_data'
local Supporters = require 'utils.datastore.supporters'
local Task = require 'utils.task_token'
local Server = require 'utils.server'

---@class CommandData
---@field name string
---@field help string
---@field aliases table
---@field parameters table
---@field parameters_count number
---@field parameters_required number
---@field check_server boolean
---@field check_backend boolean
---@field check_admin boolean
---@field check_supporter boolean
---@field check_trusted boolean
---@field check_playtime number
---@field callback function
---@field validate_self boolean
---@field validated_command boolean
---@field validate_activated boolean
---@field command_activated boolean

local this =
{
    commands = {}
}
local trace = debug.traceback

local output =
{
    backend_is_required = 'No backend is currently available. Please try again later.',
    server_is_required = 'This command requires to be run from the server.',
    admin_is_required = 'This command requires admin permissions to run.',
    supporter_is_required = 'This command requires supporter permissions to run.',
    trusted_is_required = 'This command requires trusted permissions to run.',
    playtime_is_required = 'This command requires a minimum playtime to run.',
    param_is_required = 'This command requires a parameter to run.',
    command_failed = 'Command failed to run.',
    command_success = 'Command ran successfully.',
    command_needs_validation =
    'This command requires validation to run. Please re-run the command if you wish to proceed.',
    command_needs_custom_validation =
    'This command requires validation to run. %s - please re-run the command if you wish to proceed.',
    command_is_active = 'This command is already active.',
    command_is_inactive = 'This command is already inactive.'
}

local validate_types =
{
    ['string'] = true,
    ['number'] = true,
    ['integer'] = true,
    ['boolean'] = true,
    ['player'] = true,
    ['player-online'] = true,
    ['player-admin'] = true,
    ['server'] = true,
    ['surface'] = true
}

local check_boolean =
{
    ['true'] = true,
    ['false'] = true
}

---@class MetaCommand
local Public = {}

Public.metatable = { __index = Public }

Global.register(
    this,
    function (tbl)
        this = tbl
        for _, command in pairs(this.commands) do
            setmetatable(command, Public.metatable)
        end
    end
)

local function conv(v)
    if tonumber(v) then
        return tonumber(v)
    end

    return v
end

--- Handles errors.
---@param message string
---@param notify_sound string
local function handle_error(message, notify_sound)
    message = message or ''
    Core.output_message('Command failed: ' .. message, 'warning')
    if notify_sound then
        notify_sound = notify_sound or 'utility/wire_pickup'
        if game.player then
            game.player.play_sound { path = notify_sound }
        end
    end
end

--- Handles internal errors.
---@param has_run boolean
---@param name string
---@param message string
---@return boolean
local function internal_error(has_run, name, message)
    if not has_run then
        handle_error('Action has been logged!', 'utility/cannot_build')
        if type(message) == 'string' then
            Server.output_script_data('[ERROR] Command failed to run: ' .. name .. ' - ' .. message)
        else
            Server.output_script_data('[ERROR] Command failed to run: ' .. name)
        end
    end
    return not has_run
end

---@param event EventData.on_console_command
local function execute(event)
    local command_data = this.commands[event.name] --[[@as CommandData]]

    local player
    if event.player_index and event.player_index > 0 then
        player = game.get_player(event.player_index)
    else
        player =
        {
            name = '<server>',
            position = { x = 0, y = 0 },
            surface = game.get_surface('nauvis'),
            force = game.forces.player,
            print = Server.output_script_data
        }
    end

    local is_server = event.player_index == nil

    local function reject(error_message)
        error_message = error_message or ''
        command_data.validated_command = false
        return handle_error(error_message, 'utility/cannot_build')
    end

    -- Check if player and return
    local check_server = command_data.check_server or false
    if (check_server and not is_server) and player and player.valid then
        reject(output.server_is_required)
        return
    end

    -- Check if player and return
    local check_backend = command_data.check_backend or false
    if (check_backend and not is_server) and event.player_index then
        if not Server.get_current_time() then
            reject(output.backend_is_required)
            return
        end
    end

    -- Check if the player is an admin and if the command requires it
    local check_admin = command_data.check_admin or false
    if (check_admin and not is_server) and player and not player.admin then
        reject(output.admin_is_required)
        return
    end

    -- Check if the player is trusted and if the command requires it
    local check_trusted = command_data.check_trusted or false
    if (check_trusted and not is_server) and Core.validate_player(player) then
        local is_trusted = Session.get_trusted_player(player)
        if not is_trusted then
            reject(output.trusted_is_required)
            return
        end
    end

    -- Check if the player is a supporter and if the command requires it
    local check_supporter = command_data.check_supporter or false
    if (check_supporter and not is_server) and Core.validate_player(player) then
        local is_supporter = Supporters.is_supporter(player.name)
        if not is_supporter then
            reject(output.supporter_is_required)
            return
        end
    end

    -- Check if the player has the required playtime and if the command requires it
    local check_playtime = command_data.check_playtime or false
    if (check_playtime and not is_server) and Core.validate_player(player) then
        local playtime = Session.get_session_player(player)
        if not playtime then
            reject(output.trusted_is_required)
            return
        end

        if playtime < check_playtime then
            reject(output.playtime_is_required)
            return
        end
    end

    -- Check for parameters
    if command_data.parameters_required > 0 and not event.parameter then
        reject(output.param_is_required)
        return
    end

    -- Check if the command requires the player to validate the command
    local validate_self = command_data.validate_self or false
    if validate_self and not command_data.validated_command then
        command_data.validated_command = true
        if command_data.custom_message then
            handle_error(string.format(output.command_needs_custom_validation, command_data.custom_message),
                'utility/cannot_build')
        else
            handle_error(output.command_needs_validation, 'utility/cannot_build')
        end
        return
    end

    -- Extract quoted arguments
    local input_text = event.parameter or ''
    local quoted_segments = {}

    local processed_input =
        input_text:gsub(
            '"([^"]-)"',
            function (segment)
                local no_spaces_segment = segment:gsub('%s', '%%s')
                quoted_segments[no_spaces_segment] = segment
                return ' ' .. no_spaces_segment .. ' '
            end
        )

    -- Extract unquoted arguments
    local parameters = {}
    local current_index = 0
    local parameter_count = 0

    for word in processed_input:gmatch('%S+') do
        parameter_count = parameter_count + 1
        local quoted_word = quoted_segments[word]
        local formatted_word = quoted_word and ('"' .. quoted_word .. '"') or word

        if parameter_count > command_data.parameters_count then
            parameters[current_index] = parameters[current_index] .. ' ' .. formatted_word
        else
            current_index = current_index + 1
            parameters[current_index] = formatted_word
        end
    end

    -- Check the param count
    local parameters_count = #parameters
    if parameters_count < command_data.parameters_required then
        reject(output.param_is_required)
        return
    end

    -- Parse the arguments
    local index = 1
    local handled_parameters = {}
    for _, param_data in pairs(command_data.parameters) do
        if param_data.as_type then
            local param = conv(parameters[index])
            if param_data.as_type == 'player' and param ~= nil then
                local player_name = param
                local player_data = game.get_player(player_name) --[[@type LuaPlayer]]
                if not player_data then
                    return reject('Player was not found.')
                end
                handled_parameters[index] = player_data
                index = index + 1
            end
            if param_data.as_type == 'surface' and param ~= nil then
                local surface_name = param
                if type(surface_name) ~= 'string' then
                    return reject('Inputted value is not of type string. Valid values are: "string"')
                end
                local surface_data = game.get_surface(surface_name) --[[@type LuaSurface]]
                if not surface_data then
                    return reject('Surface was not found.')
                end
                handled_parameters[index] = surface_data
                index = index + 1
            end
            if param_data.as_type == 'player-online' and param ~= nil then
                local player_name = param
                local player_data = game.get_player(player_name) --[[@type LuaPlayer]]
                if not player_data or not player_data.valid then
                    return reject('Player was not found.')
                end
                if not player_data.connected then
                    return reject('Player is not online.')
                end
                handled_parameters[index] = player_data
                index = index + 1
            end
            if param_data.as_type == 'player-admin' and param ~= nil then
                local player_name = param
                local player_data = game.get_player(player_name) --[[@type LuaPlayer]]
                if not player_data or not player_data.valid then
                    return reject('Player was not found.')
                end
                if not player_data.admin then
                    return reject('Player is not an admin.')
                end
                handled_parameters[index] = player_data
                index = index + 1
            end
            if param_data.as_type == 'server' and param ~= nil then
                local player_name = param
                local player_data = game.get_player(player_name) --[[@type LuaPlayer]]
                if player_data and player_data.valid then
                    return reject('Not running from server.')
                end
                handled_parameters[index] = player_data
                index = index + 1
            end
            if (param_data.as_type == 'number' or param_data.as_type == 'integer') and param ~= nil then
                local num = tonumber(param)
                if not num then
                    return reject('Inputted value is not of type number. Valid values are: 1, 2, 3, etc.')
                end
                handled_parameters[index] = num
                index = index + 1
            end
            if param_data.as_type == 'string' and param ~= nil then
                if type(param) ~= 'string' then
                    return reject('Inputted value is not of type string. Valid values are: "string"')
                end

                handled_parameters[index] = param
                index = index + 1
            end
            if param_data.as_type == 'boolean' and param ~= nil then
                if not check_boolean[param] then
                    return reject('Inputted value is not of type boolean. Valid values are: true, false.')
                end

                if command_data.command_activated and param == 'true' then
                    return handle_error(output.command_is_active, 'utility/cannot_build')
                end

                if not command_data.command_activated and param == 'false' then
                    return handle_error(output.command_is_inactive, 'utility/cannot_build')
                end

                handled_parameters[index] = param
                index = index + 1
            end
        end
    end

    -- Run the command callback if everything is validated
    local callback = Task.get(command_data.callback)
    local success, err = pcall(callback, player, unpack(handled_parameters))
    if internal_error(success, command_data.name, err) then
        return reject(output.command_failed)
    end

    -- Check if the command can only be run once
    local validate_activated = command_data.validate_activated or false
    if validate_activated then
        if not command_data.command_activated then
            command_data.command_activated = true
        else
            command_data.command_activated = false
        end
    end

    command_data.validated_command = false

    if err ~= nil then
        if type(err) == 'boolean' then
            if err == false then
                Core.output_message(output.command_failed, 'warning')
            else
                Core.output_message(output.command_success, 'success')
            end
        else
            Core.output_message(err)
        end
    else
        Core.output_message(output.command_success, 'success')
    end
end

--- Creates a new command.
---@param name string
---@param help string
---@return MetaCommand
function Public.new(name, help)
    if this.commands[name] then
        error('Command already exists: ' .. name, 2)
    end

    if game then error('Cannot run new() when game is initialized : ' .. name, 2) end

    local command =
        setmetatable(
            {
                name = name,
                help = help,
                aliases = {},
                parameters = {},
                parameters_count = 0,
                parameters_required = 0,
                check_admin = false,
                check_server = false,
                check_backend = false,
                check_supporter = false,
                check_trusted = false,
                check_playtime = false,
                validate_self = false,
                validated_command = false
            },
            Public.metatable
        )

    this.commands[name] = command

    return command
end

--- Requires the player to validate the command before running it.
---@param custom_message? string
---@return MetaCommand
function Public:require_validation(custom_message)
    self.validate_self = true
    if custom_message then
        self.custom_message = custom_message
    end

    return self
end

--- Requires the player to validate the command before running it.
---@return MetaCommand
function Public:is_activated()
    self.validate_activated = true
    return self
end

--- Requires the player to be an admin to run the command.
---@return MetaCommand
function Public:require_admin()
    self.check_admin = true
    return self
end

--- Requires that the command is not run from a player.
---@return MetaCommand
function Public:require_server()
    self.check_server = true
    return self
end

--- Requires that the server is connected to a backend
---@return MetaCommand
function Public:require_backend()
    self.check_backend = true
    return self
end

--- Requires the player to be a supporter to run the command.
---@return MetaCommand
function Public:require_supporter()
    self.check_supporter = true
    return self
end

--- Requires the player to be trusted to run the command.
---@return MetaCommand
function Public:require_trusted()
    self.check_trusted = true
    return self
end

--- Requires the player to have a minimum playtime to run the command.
---@param playtime integer|number
---@return MetaCommand
function Public:require_playtime(playtime)
    self.check_playtime = playtime or nil
    return self
end

---@alias ParamName
---| '"string"'
---| '"number"'
---| '"integer"'
---| '"boolean"'
---| '"player"'
---| '"player-online"'
---| '"player-admin"'
---| '"server"'
---| '"surface"'

--- Adds a parameter to the command.
---@param name string
---@param optional boolean
---@param as_type? ParamName
---@return MetaCommand
function Public:add_parameter(name, optional, as_type)
    if not validate_types[as_type] then
        error('Invalid type: ' .. as_type .. ' for parameter: ' .. name, 2)
    end

    if self.parameters[name] then
        error('Parameter: ' .. name .. ' already exists for command: ' .. self.name, 2)
    end

    self.parameters[name] = { optional = optional, as_type = as_type }
    self.parameters_count = self.parameters_count + 1

    if not optional then
        self.parameters_required = self.parameters_required + 1
    end

    return self
end

--- Adds an alias to the command.
---@param name string
---@return MetaCommand
function Public:add_alias(name)
    if self.aliases[name] then
        error('Alias: ' .. name .. ' already exists for command: ' .. self.name, 2)
    end

    self.aliases[name] = name

    return self
end

--- Sets the command as default if marking paramaters as optional.
---@param defaults any
---@return MetaCommand
function Public:set_default(defaults)
    for name, value in pairs(defaults) do
        if self.parameters[name] then
            self.parameters[name].default = value
        end
    end
    return self
end

--- Restores the command_activated state for each command
function Public.restore_states()
    for _, command in pairs(this.commands) do
        command.validated_command = false
        command.command_activated = false
    end
end

--- Registers the command to the game. Will return the player/server and the args as separate arguments.
---@param func function
function Public:callback(func)
    -- Generates a description to be used
    local description = ''
    for param_name, param_details in pairs(self.parameters) do
        if param_details.optional then
            description = string.format('%s [%s]', description, param_name)
        else
            description = string.format('%s <%s>', description, param_name)
        end
    end
    self.description = description

    -- If command fails to run, notify the player/server
    local function command_error(err)
        internal_error(false, self.name, trace(err))
    end

    -- Registers the command as a token
    local id = Task.register(func)
    self.callback = id

    -- Callback
    local function command_callback(event)
        event.name = self.name
        xpcall(execute, command_error, event)
    end

    -- Lastly, adds the command to the game
    local help = description .. ' - ' .. self.help
    commands.add_command(self.name, help, command_callback)

    -- Adds any aliases if any
    for _, alias in pairs(self.aliases) do
        if not commands.commands[alias] and not commands.game_commands[alias] then
            commands.add_command(alias, help, command_callback)
        end
    end
end

local directions =
{
    [0] = 'defines.direction.north',
    [1] = 'defines.direction.northnortheast',
    [2] = 'defines.direction.northeast',
    [3] = 'defines.direction.eastnortheast',
    [4] = 'defines.direction.east',
    [5] = 'defines.direction.eastsoutheast',
    [6] = 'defines.direction.southeast',
    [7] = 'defines.direction.southsoutheast',
    [8] = 'defines.direction.south',
    [9] = 'defines.direction.southsouthwest',
    [10] = 'defines.direction.southwest',
    [11] = 'defines.direction.westsouthwest',
    [12] = 'defines.direction.west',
    [13] = 'defines.direction.westnorthwest',
    [14] = 'defines.direction.northwest',
    [15] = 'defines.direction.northnorthwest',
}

Public.new('get', 'Hover over an object to get its name.')
    :require_admin()
    :add_parameter('die', true, 'string')
    :add_alias('entity')
    :callback(
        function (player, action)
            local entity = player.selected
            if not entity or not entity.valid then
                return false
            end

            if action and action == 'die' then
                entity.die()
                return true
            end

            player.print('[color=orange]Name:[/color] ' .. entity.name)
            player.print('[color=orange]Type:[/color] ' .. entity.type)
            player.print('[color=orange]Force:[/color] ' .. entity.force.name)
            player.print('[color=orange]Direction:[/color] ' .. entity.direction .. ' (' .. directions[entity.direction] .. ')')
            player.print('[color=orange]Destructible:[/color] ' .. (entity.destructible and 'true' or 'false'))
            player.print('[color=orange]Minable:[/color] ' .. (entity.minable and 'true' or 'false'))
            player.print('[color=orange]Unit Number:[/color] ' .. (entity.unit_number or 'nil'))
            player.print('[color=orange]Position:[/color] ' .. serpent.line(entity.position))
            return true
        end
    )

return Public
