-- lx-bundle:true

local core = require("core")
local command = {}

command.map = {}

-- Dynamic providers resolve command handlers at runtime from command name
-- Each entry uses { predicate=function, requires=table|nil, resolve=function(name), list=function|nil }
command.dynamic = {}
command.last_performed_name = nil
-- Keep one optional per-command context available to nested command calls
command.current_context = nil

local always_true = function()
	return true
end

-- Resolve a core view class module once and cache the result
-- Returns nil when the module cannot be loaded
local function get_cached_view_class(cache_key, module_name)
	command._view_class_cache = command._view_class_cache or {}
	if command._view_class_cache[cache_key] ~= nil then
		return command._view_class_cache[cache_key]
	end
	local ok, class_or_error = pcall(require, module_name)
	command._view_class_cache[cache_key] = ok and class_or_error or false
	return command._view_class_cache[cache_key] or nil
end

-- Resolve a command predicate input into a callable predicate
local function resolve_predicate(predicate)
	predicate = predicate or always_true
	if type(predicate) == "string" then
		predicate = require(predicate)
	end
	if type(predicate) == "table" then
		local class = predicate
		predicate = function()
			return core.active_view:is(class)
		end
	end
	return predicate
end

-- Evaluate one requires token against the current active view and capability APIs
local function check_requires_token(token)
	if type(token) ~= "string" or token == "" then
		return false
	end
	local active_view = core.active_view
	if token == "doc" then
		return active_view and active_view.doc ~= nil or false
	end
	if token == "select" then
		return active_view and active_view.doc and core.view_can_select(active_view) or false
	end
	if token == "edit" then
		return active_view and active_view.doc and core.view_can_edit(active_view) or false
	end
	if token == "cursor" then
		return active_view and active_view.doc and core.view_has_cursor(active_view) or false
	end
	if token == "scroll" then
		return active_view
			and type(active_view.scroll_to_line) == "function"
			and type(active_view.scroll_to_make_visible) == "function"
			or false
	end
	-- Prefer explicit built-in view token names for consistency with core module names
	if token == "command_view" then
		local command_view_class = get_cached_view_class("command_view", "core.command.view")
		return active_view and command_view_class and active_view:is(command_view_class) or false
	end
	if token == "log_view" then
		local log_view_class = get_cached_view_class("log_view", "core.log.view")
		return active_view and log_view_class and active_view:is(log_view_class) or false
	end
	if token == "binary_viewer" then
		local binary_viewer_class = get_cached_view_class("binary_viewer", "core.binary.viewer")
		return active_view and binary_viewer_class and active_view:is(binary_viewer_class) or false
	end
	if token == "image_viewer" then
		local image_viewer_class = get_cached_view_class("image_viewer", "core.image.viewer")
		return active_view and image_viewer_class and active_view:is(image_viewer_class) or false
	end
	if token == "ansi_viewer" then
		local ansi_viewer_class = get_cached_view_class("ansi_viewer", "core.ansi.viewer")
		return active_view and ansi_viewer_class and active_view:is(ansi_viewer_class) or false
	end
	if token == "text_editor" then
		local text_doc_class = get_cached_view_class("text_editor", "core.text.editor")
		return active_view and text_doc_class and active_view:is(text_doc_class) or false
	end
	-- Plugin capability token format is '@capability_name'
	-- Commands can depend on plugin-provided capabilities without hard-coding plugin names
	-- Behavior:
	-- 1) Strip '@' to get the capability key, for example '@hex_view' -> 'hex_view'
	-- 2) Fail closed when key is empty or capability registry is unavailable
	-- 3) Resolve capability API through core.capabilities.get(name)

	-- 4) If capability API exposes matches(active_view), require that to return true
	-- 5) If no matches() exists, capability presence alone satisfies the token
	if token:sub(1, 1) == "@" then
		local capability_name = token:sub(2)
		if capability_name == "" or not (core.capabilities and core.capabilities.get) then
			return false
		end
		local capability = core.capabilities.get(capability_name)
		if not capability then
			return false
		end
		if type(capability.matches) == "function" then
			return capability.matches(active_view) == true
		end
		return true
	end
	return false
end

-- Evaluate a requires list, all tokens must pass for command availability
local function check_requires(requires)
	if requires == nil then
		return true
	end
	if type(requires) ~= "table" then
		return false
	end
	for index = 1, #requires do
		if not check_requires_token(requires[index]) then
			return false
		end
	end
	return true
end

--- Normalize one command spec into executable fields
-- Keeps shared command-spec handling in one place for both registered and contributed commands
-- @param command_name string command identifier
-- @param entry table|function raw command spec or perform function
-- @param default_predicate function|table|string|nil fallback predicate input
-- @return table spec normalized command spec
function command.normalize_spec(command_name, entry, default_predicate)
	local perform = entry
	local requires = nil
	local predicate = default_predicate or always_true

	if type(entry) == "table" then
		perform = entry.perform

		requires = entry.requires
		if entry.predicate ~= nil then
			predicate = resolve_predicate(entry.predicate)
		end
	end

	assert(type(perform) == "function", "command handler must be a function: " .. tostring(command_name))
	predicate = resolve_predicate(predicate)
	return {
		perform = perform,
		requires = requires,
		predicate = predicate,
	}
end

--- Return true when one normalized command spec is currently executable
-- Checks both predicate and requires so other modules can reuse the same availability rules
-- @param spec table normalized command spec
-- @return boolean enabled true when command is currently available
function command.is_spec_enabled(spec)
	if type(spec) ~= "table" then
		return false
	end

	if type(spec.predicate) ~= "function" then
		return false
	end

	return spec.predicate() and check_requires(spec.requires)
end

-- Register commands
-- Preferred forms:
-- 1) command.add({ ['name'] = { requires = {...}, perform = function() ... end }, ... })
-- 2) command.add('name', { requires = {...}, perform = function() ... end })
-- Legacy compatibility:
-- 3) command.add(predicate, map) remains deprecated compatibility behavior
function command.add(arg1, arg2)
	-- Option 4, command.add(map)
	if arg2 == nil then
		local map = arg1
		assert(type(map) == "table", "command.add(map): map table expected")
		for command_name, command_entry in pairs(map) do
			assert(type(command_name) == "string", "command.add(map): command names must be strings")
			assert(not command.map[command_name], "command already exists: " .. command_name)
			command.map[command_name] = command.normalize_spec(command_name, command_entry, always_true)
		end
		return
	end

	-- Option 5, command.add('name', spec)
	if type(arg1) == "string" and type(arg2) == "table" and arg2.perform ~= nil then
		local command_name = arg1
		assert(not command.map[command_name], "command already exists: " .. command_name)
		command.map[command_name] = command.normalize_spec(command_name, arg2, always_true)
		return
	end

	-- Explicitly reject option 1
	if type(arg1) == "table" and arg1.requires ~= nil then
		error("command.add({ requires = ... }, map) is not supported. Use option 4 or 5.")
	end

	-- Legacy compatibility path, command.add(predicate, map)
	local predicate = resolve_predicate(arg1)
	local map = arg2
	assert(type(map) == "table", "command.add(predicate, map): map table expected")
	for command_name, command_entry in pairs(map) do
		assert(type(command_name) == "string", "command.add(predicate, map): command names must be strings")
		assert(not command.map[command_name], "command already exists: " .. command_name)
		command.map[command_name] = command.normalize_spec(command_name, command_entry, predicate)
	end
end

-- Capitalize the first character of a string
local function capitalize_first(str)
	return str:sub(1, 1):upper() .. str:sub(2)
end

-- Convert internal command ids into display-friendly names
function command.prettify_name(name)
	return name:gsub(":", ": "):gsub("-", " "):gsub("%S+", capitalize_first)
end

-- Return all command names currently valid for execution
function command.get_all_valid()
	local res = {}
	local seen = {}
	for name, cmd in pairs(command.map) do
		if command.is_spec_enabled(cmd) then
			table.insert(res, name)
			seen[name] = true
		end
	end
	-- Include dynamic command names for command palette discovery
	-- Providers may return different lists based on current active context
	for _, dyn in ipairs(command.dynamic) do
		if command.is_spec_enabled(dyn) then
			local list = dyn.list and dyn.list() or nil
			if type(list) == "table" then
				for i = 1, #list do
					local name = list[i]
					if type(name) == "string" and not seen[name] then
						table.insert(res, name)
						seen[name] = true
					end
				end
			end
		end
	end
	return res
end

-- Execute a command by name when it exists and passes its predicate
local function should_track_last_performed(name)
	if type(name) ~= "string" or name == "" then
		return false
	end
	if name == "core:find-command" then
		return false
	end
	if name == "core:repeat-last-command" or name == "core:repeat-command" then
		return false
	end
	if name:sub(1, 8) == "command:" then
		return false
	end
	return true
end

local function perform(name, context)
	local cmd = command.map[name]
	if cmd and command.is_spec_enabled(cmd) then
		local handled = cmd.perform(context)
		if handled ~= false and should_track_last_performed(name) then
			command.last_performed_name = name
		end
		return handled ~= false
	end
	-- Fall back to dynamic providers if no static command matched
	-- First provider that resolves a function handles the command
	for _, dyn in ipairs(command.dynamic) do
		if command.is_spec_enabled(dyn) then
			local fn = dyn.resolve and dyn.resolve(name) or nil
			if type(fn) == "function" then
				local handled = fn(context)
				if handled ~= false and should_track_last_performed(name) then
					command.last_performed_name = name
				end
				return handled ~= false
			end
		end
	end
	return false
end

function command.get_last_performed_name()
	return command.last_performed_name
end

--- Return the current command execution context
-- Nested command calls inherit this until an explicit override replaces it
-- @return table|any|nil context current command payload
function command.get_context()
	return command.current_context
end

-- Register a dynamic command provider that resolves handlers at execution time
function command.add_dynamic(predicate, provider)
	assert(type(provider) == "table", "dynamic provider must be a table")
	assert(type(provider.resolve) == "function", "dynamic provider requires resolve(name)")
	assert(provider.requires == nil or type(provider.requires) == "table", "dynamic provider requires list must be a table")
	local base_predicate = resolve_predicate(predicate)
	table.insert(command.dynamic, {
		predicate = function()
			return base_predicate()
		end,
		requires = provider.requires,
		resolve = provider.resolve,
		list = provider.list,
	})
end

--- Execute a command safely and report whether it was handled
-- The optional context is forwarded to new handlers and ignored by legacy ones
-- Nested calls inherit the current context unless one override is provided
-- @param name string command identifier
-- @param context table|any|nil optional command payload
-- @return boolean handled true when the command ran without returning false
function command.perform(name, context)
	local resolved_context = context

	if resolved_context == nil then
		resolved_context = command.current_context
	end

	local previous_context = command.current_context
	command.current_context = resolved_context
	command._trace_depth = (command._trace_depth or 0) + 1

	local ok, res = core.try(perform, name, resolved_context)

	command._trace_depth = math.max(0, (command._trace_depth or 1) - 1)
	command.current_context = previous_context

	return not ok or res
end

-- Load built-in command modules into the command registry
function command.add_defaults()
	-- Load built-in command modules including isolated debug command set
	local modules = {
		"core.commands",
		"core.log.commands",
		"core.debug.commands",
		"core.workspace.commands",
		"core.command.commands",
		"core.doc.commands",
		"core.text.commands",
		"core.text.findreplace",
		"core.binary.commands",
		"core.image.commands",
		"core.ansi.commands",
	}

	for _, module_name in ipairs(modules) do
		require(module_name)
	end
end

return command
