-- For "--listen" and related functionality.

local M = {}

--- Called by builtin serverlist(). Returns the combined server list (own + peers).
---
---@class vim.ServerInfo
---@field addr string Server address (socket path, named pipe, or TCP host:port).
---@field pid integer? PID of the Nvim process owning this server (nil if unreachable).
---@field own boolean True if this server belongs to the current Nvim instance.

--- @param opts? table Options:
---              - opts.peer (boolean): If true, also discover peer servers.
---              - opts.info (boolean): If true, return a list of |vim.ServerInfo| dicts
---                instead of a list of addresses. Implies peer=true.
--- @param addrs string[] Internal ("own") addresses, from server_address_list.
--- @return string[]|vim.ServerInfo[]
function M.serverlist(opts, addrs)
  if type(opts) ~= 'table' then
    return addrs
  end

  local peer = opts.peer == true
  local want_info = opts.info == true
  local discover = peer or want_info

  if not discover then
    return addrs
  end

  local own_set = {} ---@type table<string, true>
  for _, a in ipairs(addrs) do
    own_set[a] = true
  end

  -- Discover peer servers in stdpath("run").
  -- TODO: track TCP servers, somehow.
  -- TODO: support Windows named pipes.
  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 peer_pids = {} ---@type table<string, integer>

  for _, socket in ipairs(socket_paths) do
    if not own_set[socket] and not vim.list_contains(addrs, socket) then
      local ok, chan = pcall(vim.fn.sockconnect, 'pipe', socket, { rpc = true })
      if ok and chan and chan > 0 then
        -- Check that the server is responding
        -- TODO: do we need a timeout or error handling here?
        local ok_chan, chan_info = pcall(vim.fn.rpcrequest, chan, 'nvim_get_chan_info', 0)
        if ok_chan and type(chan_info) == 'table' and chan_info.id then
          table.insert(addrs, socket)
          if want_info then
            local ok_pid, pid = pcall(vim.fn.rpcrequest, chan, 'nvim_eval', 'getpid()')
            if ok_pid and type(pid) == 'number' then
              peer_pids[socket] = pid
            end
          end
        end
        pcall(vim.fn.chanclose, chan)
      end
    end
  end

  if not want_info then
    return addrs
  end

  local self_pid = vim.fn.getpid()
  local result = {} ---@type vim.ServerInfo[]
  for _, addr in ipairs(addrs) do
    local own = own_set[addr] == true
    table.insert(result, {
      addr = addr,
      pid = own and self_pid or peer_pids[addr],
      own = own,
    })
  end
  return result
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

return M
