--- @module "zync.api"
---
--- Base file of zync API
--- Contains top-level implementation of zync API:
--- 1. Arg parser
--- 2. CLI docs
--- 3. Command handling
---
local Api = {}

--- Global rock vars
_G.zync = {
    CMD = 'zync',
    NAME = 'Zync',
    VERSION = '0.2.6',
    DESCRIPTION = 'Zephyr build system companion.',
}

--- Dependencies
local argparse = require('argparse')
local lfs = require('lfs')
local utils = require('zync.lib.utils')
local configurator = require('zync.api.configurator')
local log = utils.log

--- Resolves the default/first profile name from config.
---
--- @param config zync.api.config
---
--- @return string | nil
---
local function resolve_default_profile(config)
    if not config or not config.profiles then
        return nil
    end

    if config.default_profile and config.default_profile ~= '' and config.profiles[config.default_profile] then
        return config.default_profile
    end

    if config.profile_order and #config.profile_order > 0 then
        local first = config.profile_order[1]
        if config.profiles[first] then
            return first
        end
    end

    local names = {}
    for name in pairs(config.profiles) do
        table.insert(names, name)
    end
    table.sort(names, function(a, b)
        return tostring(a) < tostring(b)
    end)
    return names[1]
end

--- Ensures compile_commands.json exists as a symlink under root/build.
---
--- Uses the default/first profile build directory as the source.
---
local function ensure_compile_commands_link()
    if not Api.config or Api.config.invalid then
        return
    end

    if not Api.config.profiles then
        return
    end

    local profile = resolve_default_profile(Api.config)
    if not profile or profile == '' then
        return
    end

    local project_dir = Api.config.project_dir
    if not project_dir or project_dir == '' then
        project_dir = lfs.currentdir()
    end

    local build_root = project_dir .. '/build'
    local link_path = build_root .. '/compile_commands.json'

    if lfs.symlinkattributes then
        local link_attr = lfs.symlinkattributes(link_path)
        if link_attr then
            if link_attr.mode == 'link' then
                return
            end
            if link_attr.mode == 'file' then
                os.remove(link_path)
            else
                return
            end
        end
    else
        if lfs.attributes(link_path) then
            return
        end
    end

    os.execute('mkdir -p ' .. build_root)

    local target = build_root .. '/' .. profile .. '/compile_commands.json'
    if lfs.link then
        local ok = lfs.link(target, link_path, true)
        if not ok then
            os.execute('ln -sf ' .. target .. ' ' .. link_path)
        end
    else
        os.execute('ln -sf ' .. target .. ' ' .. link_path)
    end
end

--- Arg parser setup
local parser = argparse(_G.zync.CMD, _G.zync.DESCRIPTION)
parser:flag('-v --version', 'Display zync version.')
parser:flag('-u --unsafe', 'Unsafe zync config execution.')
parser:flag('-c --clean', 'Clean selected profile/image build dir before running command.'):target('pre_clean')
parser:flag('-a --all', 'Run command for all configured profiles (overrides --profile/default).'):target('all')
parser:option('-p --profile', 'Select project profile defined in zconf.lua.'):argname('<profile>'):args(1)
parser:epilog([[Example:

    zync build -b nucleo_f767zi -r openocd

Or if project configured using zconf.lua:

    zync build

For more info, see https://gitlab.com/byarocks/zync.git]])

--- Zync commands built by zync.api.commands submodule
---
--- @type table<string, zync.api.commands.Command>
---
Api.commands = require('zync.api.commands.commands').build(parser)

--- @class zync.api.config
---
--- Zync config table
---
--- @field args? table              Additional args for build phase commands
--- @field cwd? string              Current working dir
--- @field is_build? boolean        Is current dir a build dir?
--- @field sysbuild? boolean        Is current dir sysbuild?
--- @field zephyr_ws? string        Zephyr workspace path
--- @field zephyr_base? string      Zephyr base path
--- @field board? string            Zephyr board config
--- @field runner? string           Zephyr runner config
--- @field profiles? table          Profile definitions
--- @field default_profile? string  Default profile name
--- @field profile? string          Selected profile name
--- @field profile_order? table     Profile order from zconf.lua
--- @field build_dir? string        Build directory relative to project
--- @field project_dir? string      Project root directory
--- @field image? number | string   Sysbuild project image
--- @field list? boolean            List sysbuild images
--- @field invalid? boolean         Invalid project dir
--- @field clean_all? boolean       Clean root build dir?
---

--- CLI arg consumption
---
--- Parsed arguments are used to derive the config and command dispatch.
---
Api.args = parser:parse()

-- Profile setup
local profile_arg = Api.args.profile
if Api.args.all then
    profile_arg = nil
end

--- @type zync.api.config
---
--- App config
---
--- Configuration is loaded once and reused by all commands for this run.
---
Api.config = configurator.init_conf(Api.args.unsafe, profile_arg)

--- Logs profile status once per run.
---
--- Skips pure metadata/list commands to avoid noisy output.
---
local function log_profile_status(config)
    local args = Api.args
    config = config or Api.config
    if args.init or args.boards or args.runners or args.profiles then
        return
    end

    if config.profile and config.profile ~= '' then
        log("Using profile '" .. config.profile .. "'")
        return
    end

    if not config.profiles then
        log('Profiles not configured - running in profile-less mode')
    end
end

--- Resolves profile list in configured order (or alphabetically).
---
--- @param config zync.api.config
---
--- @return table<string> | nil
---
local function resolve_profiles(config)
    if not config or not config.profiles then
        return nil
    end

    local profiles = {}
    if config.profile_order and #config.profile_order > 0 then
        for _, name in ipairs(config.profile_order) do
            if config.profiles[name] then
                table.insert(profiles, name)
            end
        end
    else
        for name in pairs(config.profiles) do
            table.insert(profiles, name)
        end
        table.sort(profiles, function(a, b)
            return tostring(a) < tostring(b)
        end)
    end

    return profiles
end

--- Handles zync cli version argument
---
--- Exits early when --version is requested.
---
function Api.version()
    if Api.args.version then
        print(_G.zync.CMD .. ' v' .. _G.zync.VERSION)
        os.exit(0)
    end
end

--- Runs zync
---
--- Resolves the selected command, applies pre-clean, and dispatches the handler.
---
function Api.run()
    Api.version()

    local selected_name
    local selected_command

    for name, command in pairs(Api.commands) do
        if Api.args[name] then
            selected_name = name
            selected_command = command
            break
        end
    end

    if not selected_name then
        selected_name = 'build'
        selected_command = Api.commands.build
    end

    if selected_name == 'clean' then
        Api.config.clean_all = true
    end

    if
        selected_name ~= 'init'
        and selected_name ~= 'boards'
        and selected_name ~= 'runners'
        and selected_name ~= 'profiles'
    then
        ensure_compile_commands_link()
    end

    local function log_command()
        if Api.args[selected_name] then
            log("Running command '" .. selected_name .. "'")
        else
            log("No command specified, defaulting to 'build'")
        end
    end

    local function run_for_config(config, log_cmd)
        if Api.args.pre_clean and selected_name ~= 'clean' then
            log("Running clean before '" .. selected_name .. "'")
            local cwd = lfs.currentdir()
            local original_is_build = config.is_build
            local clean_config = utils.copy_table(config)
            local pre_clean_exit = utils.normalize_exit_code(Api.commands.clean:run(Api.args, clean_config))
            lfs.chdir(cwd)
            config.is_build = original_is_build
            if pre_clean_exit ~= 0 then
                return pre_clean_exit
            end
        end

        if log_cmd then
            log_command()
        end
        log_profile_status(config)
        return utils.normalize_exit_code(selected_command:run(Api.args, config))
    end

    local profile_commands = {
        conf = true,
        build = true,
        flash = true,
        run = true,
    }

    if Api.args.all and profile_commands[selected_name] and Api.config.profiles then
        local profiles = resolve_profiles(Api.config)
        log_command()
        if profiles then
            for _, profile in ipairs(profiles) do
                local config = configurator.init_conf(Api.args.unsafe, profile)
                local exit_code = run_for_config(config, false)
                if exit_code ~= 0 then
                    os.exit(exit_code)
                end
            end
        end
        os.exit(0)
    end

    local exit_code = run_for_config(Api.config, true)
    os.exit(exit_code)
end

return Api
