---@diagnostic disable: need-check-nil
local core = require("core")
local command = require("core.command")
local fileops = require("core.file.ops")

--- Quote a path for input redirection in the platform shell
-- @param path string filesystem path
-- @return string quoted_path
local function quote_shell_path(path)
	if PATHSEP == "\\" then
		return '"' .. path .. '"'
	end

	return "'" .. path:gsub("'", "'\\''") .. "'"
end

--- Write command input to a temporary file
-- @param input string command input
-- @return string|nil path
-- @return string|nil error_message
local function write_command_input(input)
	local path, path_error = fileops.temp_path()
	if not path then
		return nil, path_error or "failed to create temporary path"
	end

	local file, open_error = io.open(path, "wb")

	if not file then
		return nil, open_error or "failed to create command input"
	end

	local write_ok, write_error = file:write(input)
	local close_ok, close_error = file:close()

	if not write_ok or not close_ok then
		os.remove(path)

		return nil, write_error or close_error or "failed to write command input"
	end

	return path
end

--- Run a shell command and capture its standard output
-- @param cmd string shell command
-- @param keep_newline boolean|nil preserve one trailing newline
-- @param input string|nil standard input
-- @return string|nil result
-- @return string|nil error_message
local function exec(cmd, keep_newline, input)
	local input_path

	if input then
		local input_error
		input_path, input_error = write_command_input(input)

		if not input_path then
			return nil, input_error
		end

		cmd = "< " .. quote_shell_path(input_path) .. " " .. cmd
	end

	local file, open_error = io.popen(cmd, "r")

	if not file then
		if input_path then
			os.remove(input_path)
		end

		return nil, open_error or "failed to start command"
	end

	local result, read_error = file:read("*a")
	local close_ok, close_reason, close_code = file:close()

	if input_path then
		os.remove(input_path)
	end

	if not result then
		return nil, read_error or "failed to read command output"
	end

	if not close_ok then
		local detail = close_code and string.format("%s (%s)", close_reason or "exit", close_code)
			or close_reason or "command failed"

		return nil, detail
	end

	return keep_newline and result or result:gsub("%\n$", "")
end

command.register({
	["exec:insert"] = {
		requires = { "doc", "edit" },
		perform = function()
		core.command_view:enter({
			caption = "Insert Result Of Command",
			submit = function(cmd)
				local result, err = exec(cmd)

				if not result then
					core.error("Command failed: %s", tostring(err))

					return
				end

				core.active_view.doc:text_input(result)
			end,
		})
		end,
	},

	["exec:replace"] = {
		requires = { "doc", "edit" },
		perform = function()
		core.command_view:enter({
			caption = "Replace With Result Of Command",
			submit = function(cmd)
				local command_error

				core.active_view.doc:replace(function(str)
					local result, err = exec(
						cmd,
						str:find("%\n$") ~= nil,
						str:gsub("%\n$", "") .. "\n"
					)

					if not result then
						command_error = err

						return str
					end

					return result
				end)

				if command_error then
					core.error("Command failed: %s", tostring(command_error))
				end
			end,
		})
		end,
	},
})
