-- lx-bundle:true
local cli = {}
local Doc = require("core.doc")
local doc_options = require("core.doc.options")

--- Format one document option snapshot for CLI logging
-- @param opts table|nil document options
-- @return string formatted
function cli.format_doc_options(opts)
	opts = type(opts) == "table" and opts or {}
	local parts = {}

	--- Append one key-value pair to a formatted option list
	-- @param key string field name
	-- @param value any field value
	local function push(key, value)
		if value == nil then
			return
		end

		parts[#parts + 1] = string.format("%s=%s", key, tostring(value))
	end

	push("name", opts.filename)
	push("syntax", opts.syntax_name)
	push("encoding", opts.encoding)
	push("codepage", opts.codepage)
	push("tab", opts.indent_size)
	push("newline", opts.newline)

	return #parts > 0 and table.concat(parts, ", ") or "<none>"
end

--- Format force-load options for CLI logging
-- @param opts table|nil force-load options
-- @return string formatted
function cli.format_force_load_options(opts)
	opts = type(opts) == "table" and opts or {}
	local parts = {}

	--- Append one force-load key-value pair to a formatted list
	-- @param key string field name
	-- @param value any field value
	local function push(key, value)
		if value == nil then
			return
		end

		parts[#parts + 1] = string.format("%s=%s", key, tostring(value))
	end

	push("force_encoding", opts.force_encoding)
	push("force_bom", opts.force_bom)
	push("force_codepage", opts.force_codepage)

	return #parts > 0 and table.concat(parts, ", ") or "<none>"
end

--- Apply a pending startup diff override to parsed open groups
-- @param core table core module
-- @param groups table parsed startup groups
-- @return table groups
local function apply_pending_diff_group_override(core, groups)
	if not (core.pending_diff_mode and core.pending_diff_files) then
		return groups
	end

	local left = core.pending_diff_files[1]
	local right = core.pending_diff_files[2]
	local left_info = left and system.get_file_info(left)
	local right_info = right and system.get_file_info(right)

	if left_info and left_info.type == "file" and right_info and right_info.type == "file" then
		local left_abs = system.absolute_path(left)
		local right_abs = system.absolute_path(right)
		core.diff_context = {
			enabled = true,
			left = left_abs,
			right = right_abs,
		}
		return {
			{ main = left_abs, split = right_abs, split_dir = "right", },
		}
	end

	return groups
end

--- Parse one CLI argv list into document-open requests and startup flags
-- @param core table core module
-- @param args table|nil argv list
-- @param opts table|nil parser mode flags
-- @return table parsed request state
function cli.parse_startup_cli_args(core, args, opts)
	args = type(args) == "table" and args or ARGS
	opts = type(opts) == "table" and opts or {}
	local allow_runtime_control_options = opts.allow_runtime_control_options ~= false
	local allow_project_dir_change = opts.allow_project_dir_change ~= false
	local allow_diff_mode = opts.allow_diff_mode ~= false

	-- Example usage:
	-- lx.exe --encoding utf8 --codepage cp437 file1.txt --tab 8 --codepage none file2.txt
	-- lx.exe -e utf16 --new -N scratch.txt --syntax lua
	-- lx.exe --defaults --new --name notes.md

	local state = {
		project_dir = EXE_DIR,
		groups = {},
		current_group = nil,
		split_next = false,
		split_dir_next = "right",
		pending_new_docs = {},
		pending_name_for_next_doc = nil,
		cli_doc_options = {},
		cli_option_errors = {},
		cli_parse_logs = {},
	}

	local function push_parse_log(fmt, ...)
		state.cli_parse_logs[#state.cli_parse_logs + 1] = string.format(fmt, ...)
	end

	local function push_option_error(fmt, ...)
		state.cli_option_errors[#state.cli_option_errors + 1] = string.format(fmt, ...)
	end

	local function consume_pending_name(doc_options)
		if type(state.pending_name_for_next_doc) == "string" and state.pending_name_for_next_doc ~= "" then
			doc_options.filename = state.pending_name_for_next_doc
			state.pending_name_for_next_doc = nil
		end
	end

	local function queue_file_target(path_token)
		local filename = system.absolute_path(path_token) or path_token
		local file_doc_options = doc_options.clone_doc_options(state.cli_doc_options)
		consume_pending_name(file_doc_options)

		if state.split_next then
			if not state.current_group then
				state.current_group = {
					main = filename,
					main_options = file_doc_options,
				}
				table.insert(state.groups, state.current_group)
			else
				state.current_group.split = filename
				state.current_group.split_dir = state.split_dir_next
				state.current_group.split_options = file_doc_options
			end
			state.split_next = false
		else
			state.current_group = {
				main = filename,
				main_options = file_doc_options,
			}
			table.insert(state.groups, state.current_group)
		end
	end

	local function parse_number_or_nil(value)
		return tonumber(value)
	end

	local option_aliases = {}

	local function register_option(names, num_values, handler)
		local option_info = {
			names = names,
			num_values = num_values,
			handler = handler,
		}
		for i = 1, #names do
			option_aliases[names[i]] = option_info
		end
	end

	register_option({ "--diff", }, 2, function(values)
		if not allow_diff_mode then
			push_parse_log("[startup-cli] ignoring %s outside startup", "--diff")
			return
		end

		local left = values[1]
		local right = values[2]
		if type(left) == "string" and type(right) == "string" then
			core.pending_diff_mode = true
			core.pending_diff_files = { left, right, }
		end
	end)

	register_option({ "--pane" }, 0, function()
		state.split_next = true
		state.split_dir_next = "right"
	end)

	register_option({ "--gridlock" }, 0, function()
		if not allow_runtime_control_options then
			push_parse_log("[startup-cli] ignoring %s outside startup", "--gridlock")
			return
		end

		core.pending_gridlock = true
	end)

	register_option({ "--no-cache", "-nc" }, 0, function()
		if not allow_runtime_control_options then
			push_parse_log("[startup-cli] ignoring %s outside startup", "--no-cache")
			return
		end

		core.pending_no_cache = true
	end)

	register_option({ "--update-cache", "-uc" }, 0, function()
		if not allow_runtime_control_options then
			push_parse_log("[startup-cli] ignoring %s outside startup", "--update-cache")
			return
		end

		core.pending_update_cache = true
	end)

	register_option({ "--run-tests", "-rt" }, 0, function()
		if not allow_runtime_control_options then
			push_parse_log("[startup-cli] ignoring %s outside startup", "--run-tests")
			return
		end

		core.pending_run_tests = true
	end)

	register_option({ "--no-tests", "-nt" }, 0, function()
		if not allow_runtime_control_options then
			push_parse_log("[startup-cli] ignoring %s outside startup", "--no-tests")
			return
		end

		core.disable_tests = true
	end)

	register_option({ "--bytes", "-b" }, 0, function()
		core.pending_binary_open_mode = "symbols"
	end)

	register_option({ "--hex", "-x" }, 0, function()
		core.pending_binary_open_mode = "hex"
	end)

	register_option({ "--view" }, 0, function()
		-- Switch later text-doc opens to the shared read-only viewer surface
		core.pending_text_view_mode = "viewer"
	end)

	register_option({ "--exit" }, 0, function()
		if not allow_runtime_control_options then
			push_parse_log("[startup-cli] ignoring %s outside startup", "--exit")
			return
		end

		core.pending_exit = true
	end)

	register_option({ "--new", "-n" }, 0, function()
		local new_doc_options = doc_options.clone_doc_options(state.cli_doc_options)
		consume_pending_name(new_doc_options)
		table.insert(state.pending_new_docs, new_doc_options)
	end)

	register_option({ "--name", "-N" }, 1, function(values)
		local name = values[1]
		if type(name) == "string" and name ~= "" then
			state.pending_name_for_next_doc = name
		end
	end)

	register_option({ "--syntax", "-s" }, 1, function(values)
		local syntax_name = values[1]
		if type(syntax_name) == "string" and syntax_name ~= "" then
			state.cli_doc_options.syntax_name = syntax_name
		end
	end)

	register_option({ "--tab-size", "--tab", "-t" }, 1, function(values, used_name)
		local n = parse_number_or_nil(values[1])
		if n then
			state.cli_doc_options.indent_size = n
		end
	end)

	register_option({ "--encoding", "-e" }, 1, function(values)
		local encoding = values[1]
		if type(encoding) == "string" and encoding ~= "" then
			if doc_options.normalize_doc_text_encoding(encoding) then
				state.cli_doc_options.encoding = encoding
			else
				push_option_error('Invalid --encoding value "%s"', encoding)
			end
		end
	end)

	register_option({ "--codepage", "-cp" }, 1, function(values)
		local codepage_name = values[1]
		if type(codepage_name) == "string" and codepage_name ~= "" then
			if doc_options.normalize_doc_codepage(codepage_name) then
				state.cli_doc_options.codepage = codepage_name
			else
				push_option_error('Invalid --codepage value "%s"', codepage_name)
			end
		end
	end)

	register_option({ "--newline", "-nl" }, 1, function(values)
		local mode = type(values[1]) == "string" and values[1]:lower() or nil
		if mode == "lf" or mode == "crlf" or mode == "auto" then
			state.cli_doc_options.newline = mode
		else
			push_option_error('Invalid --newline value "%s" (expected lf|crlf|auto)', tostring(values[1]))
		end
	end)

	register_option({ "--defaults", "-D", }, 0, function()
		-- Reset sequential CLI overrides back to configured defaults behavior
		state.cli_doc_options = {}
		state.pending_name_for_next_doc = nil
	end)

	register_option({ "--activate-tab", "-at" }, 1, function(values)
		local n = parse_number_or_nil(values[1])
		if n then
			core.pending_activate_tab = n
		end
	end)

	register_option({ "--activate-pane", "-ap" }, 1, function(values)
		local n = parse_number_or_nil(values[1])
		if n then
			core.pending_activate_pane = n
		end
	end)

	register_option({ "--activate-pane-tab", "-apt" }, 2, function(values)
		local p = parse_number_or_nil(values[1])
		local t = parse_number_or_nil(values[2])
		if p and t then
			core.pending_activate_pane = p
			core.pending_activate_tab = t
		end
	end)

	local function normalize_cli_path(path)
		if type(path) ~= "string" or path == "" then
			return nil
		end
		local absolute_path = system.absolute_path(path) or path
		absolute_path = absolute_path:gsub("\\", "/"):gsub("/+", "/")

		if #absolute_path > 1 then
			absolute_path = absolute_path:gsub("/$", "")
		end
		if absolute_path:match("^%a:/") then
			absolute_path = absolute_path:lower()
		end
		return absolute_path
	end

	local exe_norm = normalize_cli_path(EXE_FILE)
	local exe_base = exe_norm and exe_norm:match("([^/]+)$") or nil

	local function is_executable_token(arg)
		if type(arg) ~= "string" then
			return false
		end
		local normalized = normalize_cli_path(arg)
		if normalized and exe_norm and normalized == exe_norm then
			return true
		end
		local base = (normalized or arg):match("([^/\\]+)$")
		return base and exe_base and base:lower() == exe_base:lower() or false
	end

	local arg_start = 1
	if type(args[1]) == "string" and is_executable_token(args[1]) then
		arg_start = 2
	end

	local function parse_option_token(token)
		if type(token) ~= "string" then
			return nil, nil
		end

		local long_name, long_value = token:match("^(%-%-[^=]+)=(.*)$")
		if long_name then
			return long_name, long_value
		end

		local short_name, short_value = token:match("^(%-[^=]+)=(.*)$")
		if short_name then
			return short_name, short_value
		end
		return token, nil
	end

	local index = arg_start
	while index <= #args do
		local token = args[index]
		if type(token) ~= "string" then
			index = index + 1
			goto continue
		end

		local pane_dir = token:match("^%-%-pane%-(.+)$")
		if pane_dir == "left" or pane_dir == "right" or pane_dir == "up" or pane_dir == "down" then
			state.split_next = true
			state.split_dir_next = pane_dir
			index = index + 1
			goto continue
		end

		local option_name, inline_value = parse_option_token(token)
		local option_info = option_aliases[option_name]

		if option_info then
			local values = {}
			local consumed = 0

			if inline_value ~= nil then
				values[#values + 1] = inline_value
			end

			while #values < option_info.num_values do
				local next_index = index + consumed + 1
					local next_value = args[next_index]
				if type(next_value) ~= "string" then
					push_option_error('Option "%s" expects %d value(s)', option_name, option_info.num_values)
					break
				end
				values[#values + 1] = next_value
				consumed = consumed + 1
			end

			if #values == option_info.num_values then
				option_info.handler(values, option_name, token)
			end

			index = index + consumed + 1
			goto continue
		end

		local info = system.get_file_info(token) or {}

			if info.type == "file" then
				queue_file_target(token)
			elseif info.type == "dir" then
				if allow_project_dir_change then
					state.project_dir = token
				else
					push_parse_log("[startup-cli] ignoring project dir outside startup: %s", token)
				end
				state.split_next = false
			elseif token:sub(1, 1) ~= "-" then
			-- Treat non-option tokens as filename intents even if file-info resolution differs at parse time
			queue_file_target(token)
		end

		index = index + 1
		::continue::
	end

	push_parse_log(
		"[startup-cli] parse done: groups=%d pending_new=%d errors=%d project_dir=%s",
		#state.groups,
		#state.pending_new_docs,
		#state.cli_option_errors,
		tostring(state.project_dir)
	)

	return {
		project_dir = state.project_dir,
		groups = state.groups,
		pending_new_docs = state.pending_new_docs,
		pending_name_for_next_doc = state.pending_name_for_next_doc,
		cli_doc_options = state.cli_doc_options,
		cli_option_errors = state.cli_option_errors,
		cli_parse_logs = state.cli_parse_logs,
	}
end

--- Apply one parsed CLI request to the current session
-- @param core table core module
-- @param parsed table|nil parser result
-- @param opts table|nil apply options
-- @return boolean changed
function cli.apply_request(core, parsed, opts)
	parsed = type(parsed) == "table" and parsed or {}
	opts = type(opts) == "table" and opts or {}
	local apply_project_dir = opts.apply_project_dir == true
	local auto_create_doc = opts.auto_create_doc == true
	local focus_window = opts.focus_window == true
	local binary_open_mode = opts.binary_open_mode
	local activate_pane = opts.activate_pane
	local activate_tab = opts.activate_tab
	local groups = parsed.groups or {}
	local pending_new_docs = parsed.pending_new_docs or {}
	local pending_name_for_next_doc = parsed.pending_name_for_next_doc
	local cli_doc_options = parsed.cli_doc_options or {}
	local cli_option_errors = parsed.cli_option_errors or {}
	local cli_parse_logs = parsed.cli_parse_logs or {}
	local changed = false
	local command = require("core.command")

	if apply_project_dir and type(parsed.project_dir) == "string" and parsed.project_dir ~= "" then
		system.chdir(parsed.project_dir)
	end

	groups = apply_pending_diff_group_override(core, groups)

	for i = 1, #cli_parse_logs do
		core.log_quiet("%s", cli_parse_logs[i])
	end

	for i = 1, #cli_option_errors do
		core.error("%s", cli_option_errors[i])
	end

	if binary_open_mode == nil then
		binary_open_mode = core.pending_binary_open_mode
	end

	if #groups > 0 then
		local main_node = core.workspace_view:get_active_node()
		local right_node = nil

		for _, group in ipairs(groups) do
			core.set_active_view(main_node.active_view)
			local main_doc = Doc.open_file(group.main, {
				force_binary = binary_open_mode ~= nil,
				doc_options = group.main_options,
			})

			if main_doc then
				core.workspace_view:open_doc(main_doc)
				changed = true
			end

			if group.split then
				if not right_node then
					local split_doc = Doc.open_file(group.split, {
						force_binary = binary_open_mode ~= nil,
						doc_options = group.split_options,
					})
					if split_doc then
						local split_view = core.create_doc_view(split_doc)
						local dir = group.split_dir or "right"
						main_node = main_node:split(dir, split_view)
						right_node = main_node:get_parent_node(core.workspace_view.root_node).b
						core.workspace_view.root_node:update_layout()
						core.set_active_view(main_node.active_view)
						changed = true
					end
				else
					local split_doc = Doc.open_file(group.split, {
						force_binary = binary_open_mode ~= nil,
						doc_options = group.split_options,
					})
					if split_doc then
						right_node:add_view(core.create_doc_view(split_doc))
						core.workspace_view.root_node:update_layout()
						changed = true
					end
				end
			end
		end
	end

	if #pending_new_docs > 0 then
		for i = 1, #pending_new_docs do
			local startup_doc = Doc.create(nil, pending_new_docs[i])
			core.workspace_view:open_doc(startup_doc)
			changed = true
		end
	end

	if auto_create_doc and #core.docs == 0 then
		local auto_doc_opts = doc_options.clone_doc_options(cli_doc_options)

		if type(pending_name_for_next_doc) == "string" and pending_name_for_next_doc ~= "" then
			auto_doc_opts.filename = pending_name_for_next_doc
			pending_name_for_next_doc = nil
		end

		local startup_doc = Doc.create(nil, auto_doc_opts)
		core.workspace_view:open_doc(startup_doc)
		changed = true
	end

	if activate_pane ~= nil or activate_tab ~= nil then
		local saved_activate_pane = core.pending_activate_pane
		local saved_activate_tab = core.pending_activate_tab

		core.pending_activate_pane = activate_pane
		core.pending_activate_tab = activate_tab

		if core.pending_activate_pane and core.pending_activate_tab then
			command.perform("core:activate-pane-tab")
		elseif core.pending_activate_pane then
			command.perform("core:activate-pane")
		elseif core.pending_activate_tab then
			command.perform("core:activate-tab")
		end

		core.pending_activate_pane = saved_activate_pane
		core.pending_activate_tab = saved_activate_tab
	end

	if focus_window then
		system.raise_window()
		changed = true
	end

	if changed then
		core.redraw = true
	end

	return changed
end

return cli
