---This module replaces the default vim.lsp.buf.format() so that we can inject our own logic
local log = require("conform.log")
local util = require("vim.lsp.util")

local M = {}

local function apply_text_edits(text_edits, bufnr, offset_encoding, dry_run, undojoin)
  if
    #text_edits == 1
    and text_edits[1].range.start.line == 0
    and text_edits[1].range.start.character == 0
    and text_edits[1].range["end"].line >= vim.api.nvim_buf_line_count(bufnr)
    and text_edits[1].range["end"].character == 0
  then
    local original_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
    local new_lines = vim.split(text_edits[1].newText, "\r?\n", {})
    -- If it had a trailing newline, remove it to make the lines match the expected vim format
    if #new_lines > 1 and new_lines[#new_lines] == "" then
      table.remove(new_lines)
    end
    log.debug("Converting full-file LSP format to piecewise format")
    return require("conform.runner").apply_format(
      bufnr,
      original_lines,
      new_lines,
      nil,
      false,
      dry_run,
      undojoin
    )
  elseif dry_run then
    return #text_edits > 0
  else
    if undojoin then
      pcall(vim.cmd.undojoin)
    end
    vim.lsp.util.apply_text_edits(text_edits, bufnr, offset_encoding)
    return #text_edits > 0
  end
end

---@param options table
---@return table[] clients
function M.get_format_clients(options)
  local method = options.range and "textDocument/rangeFormatting" or "textDocument/formatting"

  local clients
  if vim.lsp.get_clients then
    clients = vim.lsp.get_clients({
      id = options.id,
      bufnr = options.bufnr,
      name = options.name,
      method = method,
    })
  else
    ---@diagnostic disable-next-line: deprecated
    clients = vim.lsp.get_active_clients({
      id = options.id,
      bufnr = options.bufnr,
      name = options.name,
    })

    clients = vim.tbl_filter(function(client)
      return client.supports_method(method, { bufnr = options.bufnr })
    end, clients)
  end
  if options.filter then
    clients = vim.tbl_filter(options.filter, clients)
  end
  return clients
end

---@param options conform.FormatOpts
---@param callback fun(err?: string, did_edit?: boolean)
function M.format(options, callback)
  options = options or {}
  local bufnr = options.bufnr
  if not bufnr or bufnr == 0 then
    bufnr = vim.api.nvim_get_current_buf()
    options.bufnr = bufnr
  end
  local range = options.range
  local method = range and "textDocument/rangeFormatting" or "textDocument/formatting"

  local clients = M.get_format_clients(options)

  if #clients == 0 then
    return callback("[LSP] Format request failed, no matching language servers.")
  end

  local function set_range(client, params)
    if range then
      local range_params =
        util.make_given_range_params(range.start, range["end"], bufnr, client.offset_encoding)
      params.range = range_params.range
    end
    return params
  end

  if options.async then
    local changedtick = vim.b[bufnr].changedtick
    local do_format
    local did_edit = false
    do_format = function(idx, client)
      if not client then
        return callback(nil, did_edit)
      end
      --- @diagnostic disable-next-line: param-type-mismatch
      local params = set_range(client, util.make_formatting_params(options.formatting_options))
      local auto_id = vim.api.nvim_create_autocmd("LspDetach", {
        buffer = bufnr,
        callback = function(args)
          if args.data.client_id == client.id then
            log.warn("LSP %s detached during format request", client.name)
            callback("LSP detached")
          end
        end,
      })
      local request = vim.fn.has("nvim-0.11") == 1
          and function(c, ...)
            return c:request(...)
          end
        or function(c, ...)
          return c.request(...)
        end
      request(client, method, params, function(err, result, ctx, _)
        vim.api.nvim_del_autocmd(auto_id)
        if not result then
          return callback(err or "No result returned from LSP formatter")
        elseif not vim.api.nvim_buf_is_valid(bufnr) then
          return callback("buffer was deleted")
        elseif changedtick ~= require("conform.util").buf_get_changedtick(bufnr) then
          return callback(
            string.format(
              "Async LSP formatter discarding changes for %s: concurrent modification",
              vim.api.nvim_buf_get_name(bufnr)
            )
          )
        else
          local this_did_edit = apply_text_edits(
            result,
            ctx.bufnr,
            client.offset_encoding,
            options.dry_run,
            options.undojoin
          )
          changedtick = vim.b[bufnr].changedtick

          if options.dry_run and this_did_edit then
            callback(nil, true)
          else
            did_edit = did_edit or this_did_edit
            do_format(next(clients, idx))
          end
        end
      end, bufnr)
    end
    do_format(next(clients))
  else
    local timeout_ms = options.timeout_ms or 1000
    local did_edit = false
    local request_sync = vim.fn.has("nvim-0.11") == 1
        and function(c, ...)
          return c:request_sync(...)
        end
      or function(c, ...)
        return c.request_sync(...)
      end
    for _, client in pairs(clients) do
      --- @diagnostic disable-next-line: param-type-mismatch
      local params = set_range(client, util.make_formatting_params(options.formatting_options))
      local result, wait_error = request_sync(client, method, params, timeout_ms, bufnr)
      local lsp_error = (result and result.err) or wait_error
      if result and result.result then
        local this_did_edit = apply_text_edits(
          result.result,
          bufnr,
          client.offset_encoding,
          options.dry_run,
          options.undojoin
        )
        did_edit = did_edit or this_did_edit

        if options.dry_run and did_edit then
          callback(nil, true)
          return true
        end
      elseif lsp_error then
        if not options.quiet then
          vim.notify(string.format("[LSP][%s] %s", client.name, lsp_error), vim.log.levels.WARN)
        end
        return callback(string.format("[LSP][%s] %s", client.name, lsp_error))
      end
    end
    callback(nil, did_edit)
  end
end

return M
