local fs = vim.fs
local uv = vim.uv
-- For "--listen" and related functionality.

local M = {}

--- Called by builtin serverlist(). Returns all running servers in stdpath("run").
---
--- - TODO: track TCP servers, somehow.
--- - TODO: support Windows named pipes.
---
--- @param listed string[] Already listed servers
--- @return string[] # List of servers found on the current machine in stdpath("run").
function M.serverlist(listed)
  local root = vim.fs.normalize(vim.fn.stdpath('run') .. '/..')
  local socket_paths = vim.fs.find(function(name, _)
    return name:match('nvim.*')
  end, { path = root, type = 'socket', limit = math.huge })

  local found = {} ---@type string[]
  for _, socket in ipairs(socket_paths) do
    -- Don't list servers twice
    if not vim.list_contains(listed, socket) then
      local ok, chan = pcall(vim.fn.sockconnect, 'pipe', socket, { rpc = true })
      if ok and chan then
        -- Check that the server is responding
        -- TODO: do we need a timeout or error handling here?
        if vim.fn.rpcrequest(chan, 'nvim_get_chan_info', 0).id then
          table.insert(found, socket)
        end
        vim.fn.chanclose(chan)
      end
    end
  end

  return found
end

-- (Windows only) Canonical --listen address persisted across restarts.
M.restart_canonical_addr = nil ---@type string?

--- (Windows only)
--- Called on the new server via nvim_exec_lua RPC from the old server (:restart).
--- Windows named pipes can't be rebound immediately, so the new server starts on a
--- temporary bootstrap address and polls until the canonical address is reclaimable.
--- @param canonical_addr string  The original --listen address to reclaim.
--- @param expected_uis   integer Number of UIs expected to reattach (0 = don't wait).
function M.rebind_after_restart(canonical_addr, expected_uis)
  M.restart_canonical_addr = canonical_addr
  local bootstrap_addr = vim.v.servername -- Temporary autogenerated address.
  local poll_ms = 50
  local max_wait_ms = 30000
  local timer = assert(vim.uv.new_timer())

  -- Poll until the canonical address can be reclaimed (or timeout).
  local poll_elapsed = 0
  timer:start(poll_ms, poll_ms, function()
    vim.schedule(function()
      if timer:is_closing() then
        return
      end
      poll_elapsed = poll_elapsed + poll_ms
      if poll_elapsed >= max_wait_ms then
        timer:stop()
        timer:close()
        return
      end
      if not vim.list_contains(vim.fn.serverlist(), canonical_addr) then
        local ok = vim._with({ log_level = 5 }, function()
          return pcall(vim.fn.serverstart, canonical_addr)
        end)
        if not ok then
          return -- pipe still held by old server; retry next tick
        end
      end

      -- Wait for UIs to reattach, then retire the bootstrap address.
      local elapsed = 0
      timer:stop()
      timer:start(poll_ms, poll_ms, function()
        vim.schedule(function()
          if timer:is_closing() then
            return
          end
          elapsed = elapsed + poll_ms
          local all_uis = expected_uis <= 0 or #vim.api.nvim_list_uis() >= expected_uis
          if all_uis or elapsed >= max_wait_ms then
            if canonical_addr ~= bootstrap_addr then
              vim._with({ log_level = 5 }, function()
                pcall(vim.fn.serverstop, bootstrap_addr)
              end)
            end
            timer:stop()
            timer:close()
          end
        end)
      end)
    end)
  end)
end

-- Called by ex_restart(). Saves the current session and calls back to
-- ex_restart() with the necessary arguments to restore the session.
-- TODO: https://github.com/neovim/neovim/issues/34204
function M.ex_session_restart(after_cmd, quit_cmd)
  -- Commands to run after restart
  assert(not after_cmd:find(']==]'))

  -- Use custom quit command if provided
  if quit_cmd == '' then
    quit_cmd = 'qall'
  end

  -- Preserve the value of v:this_session
  local this_session = vim.v.this_session
  assert(not this_session:find(']==]'))

  -- Get temp file to write session to
  local temp_dir = fs.abspath(fs.dirname(fs.dirname(vim.fn.tempname())))
  assert(not temp_dir:find(']==]'))
  local fd, session = uv.fs_mkstemp(fs.joinpath(temp_dir, 'restart_session_XXXXXX'))
  if not fd then
    error('Failed to get temporary filename for restart session')
  end
  uv.fs_close(fd)

  -- Write session
  local session_arg = vim.fn.fnameescape(session)
  vim.cmd('%argdelete')
  vim.cmd.mksession { session_arg, bang = true }

  -- Lua commands to restore the session and remove the session file
  local after_list = {}
  table.insert(after_list, ('vim.cmd("source %s")'):format(session_arg))
  table.insert(after_list, ('pcall(vim.fs.rm, [==[%s]==])'):format(session))
  table.insert(after_list, ('vim.v.this_session = [==[%s]==]'):format(this_session))
  -- User provided command
  if after_cmd ~= '' then
    table.insert(after_list, ('vim.cmd([==[%s]==])'):format(after_cmd))
  end
  -- Concatenate everything together
  local after = 'lua ' .. table.concat(after_list, ';')

  -- Restart Neovim and run our Lua commands
  local success, msg = pcall(function()
    -- "+:::" special argument tells the C handler that this is actually a non-bang restart
    -- That way, v:startreason can be set correctly
    vim.cmd.restart { '+:::', quit_cmd, after, bang = true }
  end)

  if not success then
    --- @cast msg string
    fs.rm(session, { force = true })

    -- Trim error message to be equivalent to `:restart!`
    local trimmed_msg = msg:match('Vim:.*$')
    if trimmed_msg then
      vim.api.nvim_echo({ { trimmed_msg:sub(5) } }, true, { err = true })
    else
      error(msg)
    end
  end
end

return M
