---@diagnostic disable: need-check-nil

--- Generate keymap overview documents from runtime key binding state
-- Keeps export data shared while output formatting stays separate
-- @module plugins.keymap

local core = require("core")
local command = require("core.command")
local Doc = require("core.doc")
local keymap = require("core.keymap")

--- Escape one markdown cell value
-- @param text any source value
-- @return string escaped text
local function md_escape(text)
	local escaped_text = tostring(text or "")
	escaped_text = escaped_text:gsub("\\", "\\\\")
	escaped_text = escaped_text:gsub("|", "\\|")

	return escaped_text
end

--- Pad one string on the right for table output
-- @param text any source value
-- @param width number target width
-- @return string padded text
local function pad_right(text, width)
	local padded_text = tostring(text or "")
	local missing_width = width - #padded_text

	if missing_width > 0 then
		return padded_text .. string.rep(" ", missing_width)
	end

	return padded_text
end

--- Join command ids for markdown table cells
-- @param commands table command ids
-- @return string cell text
local function join_markdown_commands(commands)
	if type(commands) ~= "table" or #commands == 0 then
		return ""
	end

	local out = {}

	for index = 1, #commands do
		out[#out + 1] = "'" .. md_escape(commands[index]) .. "'"
	end

	return table.concat(out, ", ")
end

--- Join command ids for multi-line text table cells
-- @param commands table command ids
-- @return string cell text
local function join_text_commands(commands)
	if type(commands) ~= "table" or #commands == 0 then
		return ""
	end

	local out = {}
	out[1] = tostring(commands[1] or "")

	for index = 2, #commands do
		out[index] = string.rep(" ", index - 1) .. "-> " .. tostring(commands[index] or "")
	end

	return table.concat(out, "\n")
end

--- Split one cell into physical lines for text-table layout
-- @param text any source value
-- @return table lines split lines
local function split_lines(text)
	local source_text = tostring(text or "")

	if source_text == "" then
		return { "" }
	end

	local lines = {}

	for line in (source_text .. "\n"):gmatch("(.-)\n") do
		lines[#lines + 1] = line
	end

	return lines
end

--- Append one aligned markdown table
-- @param lines table output lines
-- @param headers table table headers
-- @param rows table row arrays
-- @return nil
local function append_markdown_table(lines, headers, rows)
	local widths = {}

	for index = 1, #headers do
		widths[index] = #(headers[index] or "")
	end

	for row_index = 1, #rows do
		local row = rows[row_index]

		for column_index = 1, #headers do
			local cell = tostring(row[column_index] or "")

			if #cell > widths[column_index] then
				widths[column_index] = #cell
			end
		end
	end

	local header_cells = {}
	local divider_cells = {}

	for index = 1, #headers do
		header_cells[index] = " " .. pad_right(headers[index], widths[index]) .. " "
		divider_cells[index] = string.rep("-", widths[index] + 2)
	end

	lines[#lines + 1] = "|" .. table.concat(header_cells, "|") .. "|"
	lines[#lines + 1] = "|" .. table.concat(divider_cells, "|") .. "|"

	for row_index = 1, #rows do
		local row = rows[row_index]
		local cells = {}

		for column_index = 1, #headers do
			cells[column_index] = " " .. pad_right(row[column_index] or "", widths[column_index]) .. " "
		end

		lines[#lines + 1] = "|" .. table.concat(cells, "|") .. "|"
	end
end

--- Append one aligned multi-line text table
-- @param lines table output lines
-- @param headers table table headers
-- @param rows table row arrays
-- @return nil
local function append_text_table(lines, headers, rows)
	local min_widths = { 16, 16, 32 }
	local widths = {}

	for index = 1, #headers do
		widths[index] = math.max(#(headers[index] or ""), min_widths[index] or 0)
	end

	for row_index = 1, #rows do
		local row = rows[row_index]

		for column_index = 1, #headers do
			local cell_lines = split_lines(row[column_index])

			for line_index = 1, #cell_lines do
				local cell = cell_lines[line_index]

				if #cell > widths[column_index] then
					widths[column_index] = #cell
				end
			end
		end
	end

	local function make_row(row, line_index)
		local cells = {}

		for column_index = 1, #headers do
			local cell_lines = split_lines(row[column_index])
			local cell = cell_lines[line_index] or ""
			cells[column_index] = " " .. pad_right(cell, widths[column_index]) .. " "
		end

		return "|" .. table.concat(cells, "|") .. "|"
	end

	local divider_cells = {}

	for index = 1, #headers do
		divider_cells[index] = string.rep("-", widths[index] + 2)
	end

	lines[#lines + 1] = make_row(headers, 1)
	lines[#lines + 1] = "|" .. table.concat(divider_cells, "|") .. "|"

	for row_index = 1, #rows do
		local row = rows[row_index]
		local row_height = 1

		for column_index = 1, #headers do
			local cell_height = #split_lines(row[column_index])

			if cell_height > row_height then
				row_height = cell_height
			end
		end

		for line_index = 1, row_height do
			lines[#lines + 1] = make_row(row, line_index)
		end
	end
end

--- Build markdown table rows from export rows
-- @param export_rows table structured rows
-- @return table rows markdown rows
local function build_markdown_rows(export_rows)
	local rows = {}

	for index = 1, #export_rows do
		local row = export_rows[index]
		local commands = join_markdown_commands(row.commands)

		-- Reassignment rows name their other key instead of listing commands copied at runtime
		if row.operation == "swap" or row.operation == "move" or row.operation == "copy" then
			commands = "'" .. md_escape(row.paired_stroke) .. "'"
		end

		rows[#rows + 1] = {
			"'" .. md_escape(row.stroke) .. "'",
			md_escape(row.operation),
			commands,
		}
	end

	return rows
end

--- Build text table rows from export rows
-- @param export_rows table structured rows
-- @return table rows text rows
local function build_text_rows(export_rows)
	local rows = {}

	for index = 1, #export_rows do
		local row = export_rows[index]
		local commands = join_text_commands(row.commands)

		-- Keep plain-text exports as clear as the markdown version
		if row.operation == "swap" or row.operation == "move" or row.operation == "copy" then
			commands = tostring(row.paired_stroke or "")
		end

		rows[#rows + 1] = {
			tostring(row.context or ""),
			tostring(row.stroke or ""),
			tostring(row.operation or ""),
			commands,
		}
	end

	return rows
end

--- Render one markdown export document from structured keymap data
-- @param export_data table structured export snapshot
-- @return string content markdown content
local function build_keymap_markdown(export_data)
	local lines = {
		"# Keyboard Bindings Overview",
		"",
		string.format("- Generated: '%s'", export_data.generation_time),
		"- Source: runtime 'core.keymap' state",
		"",
		"## Effective Bindings",
		"",
	}

	for context_index = 1, #export_data.contexts do
		local context_section = export_data.contexts[context_index]
		lines[#lines + 1] = string.format("### Context '%s'", md_escape(context_section.name))
		lines[#lines + 1] = ""
		lines[#lines + 1] = string.format("- Total strokes: **%d**", #context_section.effective_rows)
		lines[#lines + 1] = ""
		append_markdown_table(lines, { "Stroke", "Operation", "Commands or paired shortcut" }, build_markdown_rows(context_section.effective_rows))
		lines[#lines + 1] = ""
	end

	lines[#lines + 1] = "## Layered Bindings"
	lines[#lines + 1] = ""

	for context_index = 1, #export_data.contexts do
		local context_section = export_data.contexts[context_index]
		lines[#lines + 1] = string.format("### Context '%s'", md_escape(context_section.name))
		lines[#lines + 1] = ""

		for layer_index = 1, #context_section.layer_sections do
			local layer_section = context_section.layer_sections[layer_index]
			lines[#lines + 1] = string.format("#### Layer '%s'", md_escape(layer_section.layer))
			lines[#lines + 1] = ""
			lines[#lines + 1] = string.format("- Total declarations: **%d**", #layer_section.rows)
			lines[#lines + 1] = ""
			append_markdown_table(lines, { "Stroke", "Operation", "Commands or paired shortcut" }, build_markdown_rows(layer_section.rows))
			lines[#lines + 1] = ""
		end
	end

	return table.concat(lines, "\n")
end

--- Render one plain-text export document from structured keymap data
-- @param export_data table structured export snapshot
-- @return string content text content
local function build_keymap_text(export_data)
	local lines = {
		"Keyboard Bindings Overview",
		"",
		string.format("Generated: %s", export_data.generation_time),
		"Source: runtime core.keymap state",
		"",
		"Effective Bindings",
		"",
	}

	for context_index = 1, #export_data.contexts do
		local context_section = export_data.contexts[context_index]
		lines[#lines + 1] = string.format("Context: %s (%d)", context_section.name, #context_section.effective_rows)
		lines[#lines + 1] = ""
		append_text_table(lines, { "Context", "Shortcut", "Operation", "Commands or paired shortcut" }, build_text_rows(context_section.effective_rows))
		lines[#lines + 1] = ""
	end
	lines[#lines + 1] = "Layered Bindings"
	lines[#lines + 1] = ""

	for context_index = 1, #export_data.contexts do
		local context_section = export_data.contexts[context_index]
		lines[#lines + 1] = string.format("Context: %s", context_section.name)
		lines[#lines + 1] = ""

		for layer_index = 1, #context_section.layer_sections do
			local layer_section = context_section.layer_sections[layer_index]
			lines[#lines + 1] = string.format("Layer: %s (%d declarations)", layer_section.layer, #layer_section.rows)
			lines[#lines + 1] = ""
			append_text_table(lines, { "Context", "Shortcut", "Operation", "Commands or paired shortcut" }, build_text_rows(layer_section.rows))
			lines[#lines + 1] = ""
		end
	end

	return table.concat(lines, "\n")
end

--- Open one generated document in a new editor tab
-- @param filename string generated document name
-- @param content string generated text
-- @return nil
local function open_generated_doc(filename, content)
	local doc = Doc.create(filename)
	core.workspace_view:open_doc(doc)
	local last_line = #doc.lines
	local last_col = #(doc.lines[last_line] or "") + 1
	doc:remove(1, 1, last_line, last_col)
	doc:insert(1, 1, content)
	doc:set_selection(1, 1)
end

--- Generate and open the plain-text keymap overview
-- @return nil
local function generate_text_doc()
	local export_data = keymap.build_export_data()
	open_generated_doc("keymap.txt", build_keymap_text(export_data))
end

--- Generate and open the markdown keymap overview
-- @return nil
local function generate_markdown_doc()
	local export_data = keymap.build_export_data()
	open_generated_doc("keymap.md", build_keymap_markdown(export_data))
end

command.register({
	["keymap:generate-txt"] = {
		perform = function()
			generate_text_doc()
		end,
	},

	["keymap:generate-md"] = {
		perform = function()
			generate_markdown_doc()
		end,
	},
})
