--- @class zync.api.commands.Command
---
--- Zync CLI command base class
---
--- @field parser any       Argparse parser
--- @field command any      Command metadata
--- @field args any         Table of arg metadata
--- @field handler function Command runtime handler
--- @field name string
---
local Command = {}

--- @class zync.api.commands.arg
---
--- Zync CLI command arg
---
--- @field flag? string             Arg flag
--- @field desc string              Arg description
--- @field arg? string              Arg value name
--- @field optional? boolean        True = arg optional
--- @field skip_if? string          Skip arg if a specified argument provided
--- @field nargs? number | string   Number of args consumed (e.g.: 1, 3, '?', '*')
--- @field commands? table          Commands to initialize arg for

--- Create class
Command.__index = Command

--- Dependencies
local utils = require('zync.lib.utils')

--- Command constructor
---
--- @param parser any       Argparse parser
--- @param name string      Command name
--- @param short string     Command shorthand
--- @param summary string   Command summary
--- @param desc? string     Command description
--- @param handler function Runtime handler to execute after parse
---
--- @return zync.api.commands.Command Command object
---
function Command:new(parser, name, short, summary, desc, handler)
    local obj = setmetatable({}, self)

    -- set up argparse command based on command obj metadata
    obj.name = name
    obj.parser = parser
    obj.command = parser:require_command(false):command(name .. ' ' .. short):summary(summary)

    if desc and desc ~= '' then
        obj.command:description(desc)
    end

    -- set up runtime handler function
    obj.handler = handler

    return obj
end

--- Returns a table of sorted command args
---
--- @return table<zync.api.commands.arg>    Table of sorted arguments
---
function Command:sorted_args()
    local sorted = {}
    for arg, opts in pairs(self.args) do
        table.insert(sorted, { arg = arg, opts = opts })
    end
    table.sort(sorted, function(a, b)
        return a.arg < b.arg
    end)

    return sorted
end

--- Creates command argument tag
---
--- @param arg string       Argument name
--- @param opts table<any>  Table of argument opts
---
--- @return string Argument tag
---
local function create_arg(arg, opts)
    local carg = ''
    if string.len(arg) > 1 then
        carg = '--' .. arg
    end

    if opts.flag and opts.flag ~= '' and string.len(opts.flag) == 1 then
        carg = carg .. ' ' .. '-' .. opts.flag
    end

    return carg
end

--- Sets up command args
---
--- @param args table<string, zync.api.commands.arg> Table of command args
---
--- @return zync.api.commands.Command Command object
---
function Command:args(args)
    self.args = args or {}

    -- sort command args before setting up
    local sorted_args = self:sorted_args()

    -- set up args for argparse commands based on command obj metadata
    for _, sarg in pairs(sorted_args) do
        local arg = sarg.arg
        local opts = sarg.opts
        if opts.commands and opts.commands ~= {} then
            local found = false
            for _, cmd in ipairs(opts.commands) do
                if cmd == self.name then
                    found = true
                end
            end
            if not found then
                goto skip_arg
            end
        end
        local tag = create_arg(arg, opts)

        -- set up arg based on whether its a flag/option or takes an arg
        if opts.arg then
            local opt = self.command:option(tag, opts.desc):argname('<' .. opts.arg .. '>')
            if opts.nargs and opts.nargs ~= '' then
                opt:args(opts.nargs)
            else
                opt:args(1)
            end
            opt:convert(utils.tonumber_if_possible)
        else
            self.command:flag(tag, opts.desc)
        end
        ::skip_arg::
    end

    return self
end

--- Parses command arguments
---
--- @param args table<any>      Table of args consumed by argparse
--- @param config table<any>    App config table
---
--- @return zync.api.commands.Command   self
---
function Command:parse(args, config)
    -- get sorted argument table
    local sorted_args = self:sorted_args()

    -- set up app config based on args consumed by argparse
    for _, sarg in ipairs(sorted_args) do
        local name = sarg.arg:gsub('%s+$', '')
        local opts = sarg.opts
        local val = args[name]

        if opts.skip_if and args[opts.skip_if] then
            goto continue
        end

        if val and val ~= '' then
            config[name] = val
        end

        ::continue::
    end

    return self
end

--- Runs command handler
---
--- @param args table<any>      Table of args consumed by argparse
--- @param config table<any>    App config table
---
--- @return zync.api.commands.Command self
---
function Command:run(args, config)
    -- parse args and call command runtime handler
    self:parse(args, config)
    return self.handler(config)
end

return Command
