local augroup = vim.api.nvim_create_augroup("UserConfig", { clear = false })

-- Diagnostic icons shown in the sign column and virtual text
local diagnostic_signs = {
	Error = " ",
	Warn = " ",
	Hint = "",
	Info = "",
}

-- Global diagnostic display options
vim.diagnostic.config({
	virtual_text = { prefix = "●", spacing = 4 }, -- inline diagnostic text to the right
	signs = {
		text = {
			[vim.diagnostic.severity.ERROR] = diagnostic_signs.Error,
			[vim.diagnostic.severity.WARN] = diagnostic_signs.Warn,
			[vim.diagnostic.severity.INFO] = diagnostic_signs.Info,
			[vim.diagnostic.severity.HINT] = diagnostic_signs.Hint,
		},
	},
	underline = true,
	update_in_insert = false, -- do not update diagnostics while typing (reduces noise)
	severity_sort = true,     -- show errors before warnings in lists
	float = {
		border = "rounded",
		source = "always", -- always show which LSP server produced the diagnostic
		header = "",
		prefix = "",
		focusable = false,
		style = "minimal",
	},
})

-- Patch vim.lsp.util.open_floating_preview so every LSP floating window
-- (hover, signature help, etc.) gets a rounded border without having to
-- configure it per-server.
do
	local orig = vim.lsp.util.open_floating_preview
	function vim.lsp.util.open_floating_preview(contents, syntax, opts, ...)
		opts = opts or {}
		opts.border = opts.border or "rounded"
		return orig(contents, syntax, opts, ...)
	end
end

-- lsp_on_attach is called whenever an LSP server attaches to a buffer.
-- It sets up buffer-local keymaps that are only active when an LSP is present.
local function lsp_on_attach(ev)
	local client = vim.lsp.get_client_by_id(ev.data.client_id)
	if not client then
		return
	end

	local bufnr = ev.buf
	local function map(key, fn, desc)
		vim.keymap.set("n", key, fn, { noremap = true, silent = true, buffer = bufnr, desc = desc })
	end

	-- Navigation
	map("<leader>gd", vim.lsp.buf.definition,                                                              "Go to definition")
	map("<leader>gS", function() vim.cmd("vsplit") vim.lsp.buf.definition() end,                           "Go to definition (vsplit)")

	-- Code actions and refactoring
	map("<leader>ca", vim.lsp.buf.code_action, "Code action")
	map("<leader>rn", vim.lsp.buf.rename,      "Rename symbol")

	-- Diagnostics
	map("<leader>D",  function() vim.diagnostic.open_float({ scope = "line" })   end, "Line diagnostics")
	map("<leader>d",  function() vim.diagnostic.open_float({ scope = "cursor" }) end, "Cursor diagnostic")
	map("<leader>nd", function() vim.diagnostic.jump({ count = 1 })              end, "Next diagnostic")
	map("<leader>pd", function() vim.diagnostic.jump({ count = -1 })             end, "Prev diagnostic")

	-- Hover documentation
	map("K", vim.lsp.buf.hover, "Hover docs")

	-- fzf-lua LSP pickers
	map("<leader>fd", function() require("fzf-lua").lsp_definitions({ jump_to_single_result = true }) end, "Definitions")
	map("<leader>fr", function() require("fzf-lua").lsp_references()         end, "References")
	map("<leader>ft", function() require("fzf-lua").lsp_typedefs()           end, "Type definitions")
	map("<leader>fs", function() require("fzf-lua").lsp_document_symbols()   end, "Document symbols")
	map("<leader>fw", function() require("fzf-lua").lsp_workspace_symbols()  end, "Workspace symbols")
	map("<leader>fi", function() require("fzf-lua").lsp_implementations()    end, "Implementations")

	-- Organise imports then format (only registered when the server supports codeAction)
	if client:supports_method("textDocument/codeAction", bufnr) then
		map("<leader>oi", function()
			vim.lsp.buf.code_action({
				context = { only = { "source.organizeImports" }, diagnostics = {} },
				apply = true,
				bufnr = bufnr,
			})
			-- Small delay lets the import action settle before formatting
			vim.defer_fn(function()
				vim.lsp.buf.format({ bufnr = bufnr })
			end, 50)
		end, "Organize imports")
	end
end

vim.api.nvim_create_autocmd("LspAttach", { group = augroup, callback = lsp_on_attach })

-- Global diagnostic keymaps (available without an LSP attached)
vim.keymap.set("n", "<leader>q", function() vim.diagnostic.setloclist({ open = true }) end, { desc = "Open diagnostic list" })

-- blink.cmp: completion engine wired to LuaSnip for snippet expansion
require("blink.cmp").setup({
	enabled = function()
		return not vim.tbl_contains({ "markdown", "text" }, vim.bo.filetype)
	end,
	keymap = {
		preset = "none", -- start from a blank slate; all bindings are explicit
		["<C-Space>"] = { "show", "hide" },      -- toggle the completion menu
		["<CR>"] = { "accept", "fallback" },     -- confirm selection
		["<C-j>"] = { "select_next", "fallback" },
		["<C-k>"] = { "select_prev", "fallback" },
		["<Tab>"] = { "snippet_forward", "fallback" },   -- jump to next snippet placeholder
		["<S-Tab>"] = { "snippet_backward", "fallback" }, -- jump to previous placeholder
	},
	appearance = { nerd_font_variant = "mono" },
	completion = { menu = { auto_show = true } }, -- show the menu automatically (no manual trigger needed)
	sources = { default = { "lsp", "path", "buffer", "snippets" } },
	snippets = {
		-- Delegate snippet expansion to LuaSnip
		expand = function(snippet)
			require("luasnip").lsp_expand(snippet)
		end,
	},
	fuzzy = {
		implementation = "prefer_rust", -- use the faster Rust fuzzy-matching backend when available
		prebuilt_binaries = { download = true }, -- automatically download the pre-built binary
	},
})

-- Inject blink.cmp capabilities into every LSP server so they can offer
-- completions that respect snippet and LSP-specific completion item fields
vim.lsp.config["*"] = {
	capabilities = require("blink.cmp").get_lsp_capabilities(),
}

-- Server configurations
vim.lsp.config("lua_ls", {
	settings = {
		Lua = {
			diagnostics = { globals = { "vim" } }, -- teach lua_ls that `vim` is a known global
			telemetry = { enable = false },
		},
	},
})
vim.lsp.config("pyright", {})  -- Python type checker and language server
vim.lsp.config("bashls", {})   -- Bash language server
vim.lsp.config("vtsls", {})    -- TypeScript / JavaScript language server (VS Code's TS server)
vim.lsp.config("gopls", {})    -- Go language server
vim.lsp.config("omnisharp", {}) -- C# language server

-- EFM (efm-langserver): a generic LSP adapter that runs command-line linters
-- and formatters and exposes them as LSP capabilities.
do
	local luacheck  = require("efmls-configs.linters.luacheck")
	local stylua    = require("efmls-configs.formatters.stylua")
	local flake8    = require("efmls-configs.linters.flake8")
	local black     = require("efmls-configs.formatters.black")
	local prettier_d = require("efmls-configs.formatters.prettier_d")
	local eslint_d  = require("efmls-configs.linters.eslint_d")
	local fixjson   = require("efmls-configs.formatters.fixjson")
	local shellcheck = require("efmls-configs.linters.shellcheck")
	local shfmt     = require("efmls-configs.formatters.shfmt")
	local go_revive = require("efmls-configs.linters.go_revive")
	local gofumpt   = require("efmls-configs.formatters.gofumpt")
	local csharpier = { formatCommand = "dotnet csharpier --write-stdout", formatStdin = true }

	vim.lsp.config("efm", {
		filetypes = {
			"cs", "css", "go", "html",
			"javascript", "javascriptreact",
			"json", "jsonc",
			"lua", "markdown", "python", "sh",
			"typescript", "typescriptreact",
		},
		init_options = { documentFormatting = true },
		settings = {
			languages = {
				cs             = { csharpier },
				go             = { gofumpt, go_revive },
				css            = { prettier_d },
				html           = { prettier_d },
				javascript     = { eslint_d, prettier_d },
				javascriptreact = { eslint_d, prettier_d },
				json           = { eslint_d, fixjson },
				jsonc          = { eslint_d, fixjson },
				lua            = { luacheck, stylua },
				markdown       = { prettier_d },
				python         = { flake8, black },
				sh             = { shellcheck, shfmt },
				typescript     = { eslint_d, prettier_d },
				typescriptreact = { eslint_d, prettier_d },
			},
		},
	})
end

-- Start all configured LSP servers
vim.lsp.enable({
	"lua_ls",
	"pyright",
	"bashls",
	"vtsls",
	"gopls",
	"omnisharp",
	"efm",
})
