---
-- Visual command handling module for ClaudeCode.nvim
-- Implements neo-tree-style visual mode exit and command processing
-- @module claudecode.visual_commands
local M = {}

--- Get current vim mode with fallback for test environments
--- @param full_mode boolean|nil Whether to get full mode info (passed to vim.fn.mode)
--- @return string current_mode The current vim mode
local function get_current_mode(full_mode)
  local current_mode = "n" -- Default fallback

  pcall(function()
    if vim.api and vim.api.nvim_get_mode then
      current_mode = vim.api.nvim_get_mode().mode
    else
      current_mode = vim.fn.mode(full_mode)
    end
  end)

  return current_mode
end

-- ESC key constant matching neo-tree's implementation
local ESC_KEY
local success = pcall(function()
  ESC_KEY = vim.api.nvim_replace_termcodes("<Esc>", true, false, true)
end)
if not success then
  ESC_KEY = "\27"
end

--- Exit visual mode properly and schedule command execution
--- @param callback function The function to call after exiting visual mode
--- @param ... any Arguments to pass to the callback
function M.exit_visual_and_schedule(callback, ...)
  local args = { ... }

  -- Capture visual selection data BEFORE exiting visual mode
  local visual_data = M.capture_visual_selection_data()

  pcall(function()
    vim.api.nvim_feedkeys(ESC_KEY, "i", true)
  end)

  -- Schedule execution until after mode change (neo-tree pattern)
  local schedule_fn = vim.schedule or function(fn)
    fn()
  end -- Fallback for test environments
  schedule_fn(function()
    -- Pass the captured visual data as the first argument
    callback(visual_data, unpack(args))
  end)
end

--- Validate that we're currently in a visual mode
--- @return boolean true if in visual mode, false otherwise
--- @return string|nil error message if not in visual mode
function M.validate_visual_mode()
  local current_mode = get_current_mode(true)

  local is_visual = current_mode == "v" or current_mode == "V" or current_mode == "\022"

  -- Additional debugging: check visual marks and cursor position
  if is_visual then
    pcall(function()
      vim.api.nvim_win_get_cursor(0)
      vim.fn.getpos("'<")
      vim.fn.getpos("'>")
      vim.fn.getpos("v")
    end)
  end

  if not is_visual then
    return false, "Not in visual mode (current mode: " .. current_mode .. ")"
  end

  return true, nil
end

--- Get visual selection range using vim marks or current cursor position
--- @return number, number start_line, end_line (1-indexed)
function M.get_visual_range()
  local start_pos, end_pos = 1, 1 -- Default fallback

  -- Use pcall to handle test environments
  local range_success = pcall(function()
    -- Check if we're currently in visual mode
    local current_mode = get_current_mode(true)
    local is_visual = current_mode == "v" or current_mode == "V" or current_mode == "\022"

    if is_visual then
      -- In visual mode, ALWAYS use cursor + anchor (marks are stale until exit)
      local cursor_pos = vim.api.nvim_win_get_cursor(0)[1]
      local anchor_pos = vim.fn.getpos("v")[2]

      if anchor_pos > 0 then
        start_pos = math.min(cursor_pos, anchor_pos)
        end_pos = math.max(cursor_pos, anchor_pos)
      else
        -- Fallback: just use current cursor position
        start_pos = cursor_pos
        end_pos = cursor_pos
      end
    else
      -- Not in visual mode, try to use the marks (they should be valid now)
      local mark_start = vim.fn.getpos("'<")[2]
      local mark_end = vim.fn.getpos("'>")[2]

      if mark_start > 0 and mark_end > 0 then
        start_pos = mark_start
        end_pos = mark_end
      else
        -- No valid marks, use cursor position
        local cursor_pos = vim.api.nvim_win_get_cursor(0)[1]
        start_pos = cursor_pos
        end_pos = cursor_pos
      end
    end
  end)

  if not range_success then
    return 1, 1
  end

  if end_pos < start_pos then
    start_pos, end_pos = end_pos, start_pos
  end

  -- Ensure we have valid line numbers (at least 1)
  start_pos = math.max(1, start_pos)
  end_pos = math.max(1, end_pos)

  return start_pos, end_pos
end

--- Check if we're in a tree buffer and get the tree state
--- @return table|nil, string|nil tree_state, tree_type ("neo-tree" or "nvim-tree")
function M.get_tree_state()
  local current_ft = "" -- Default fallback
  local current_win = 0 -- Default fallback

  -- Use pcall to handle test environments
  local state_success = pcall(function()
    current_ft = vim.bo.filetype or ""
    current_win = vim.api.nvim_get_current_win()
  end)

  if not state_success then
    return nil, nil
  end

  if current_ft == "neo-tree" then
    local manager_success, manager = pcall(require, "neo-tree.sources.manager")
    if not manager_success then
      return nil, nil
    end

    local state = manager.get_state("filesystem")
    if not state then
      return nil, nil
    end

    -- Validate we're in the correct neo-tree window
    if state.winid and state.winid == current_win then
      return state, "neo-tree"
    else
      return nil, nil
    end
  elseif current_ft == "NvimTree" then
    local api_success, nvim_tree_api = pcall(require, "nvim-tree.api")
    if not api_success then
      return nil, nil
    end

    return nvim_tree_api, "nvim-tree"
  elseif current_ft == "oil" then
    local oil_success, oil = pcall(require, "oil")
    if not oil_success then
      return nil, nil
    end

    return oil, "oil"
  else
    return nil, nil
  end
end

--- Create a visual command wrapper that follows neo-tree patterns
--- @param normal_handler function The normal command handler
--- @param visual_handler function The visual command handler
--- @return function The wrapped command function
function M.create_visual_command_wrapper(normal_handler, visual_handler)
  return function(...)
    local current_mode = get_current_mode(true)

    if current_mode == "v" or current_mode == "V" or current_mode == "\022" then
      -- Use the neo-tree pattern: exit visual mode, then schedule execution
      M.exit_visual_and_schedule(visual_handler, ...)
    else
      normal_handler(...)
    end
  end
end

--- Capture visual selection data while still in visual mode
--- @return table|nil visual_data Captured data or nil if not in visual mode
function M.capture_visual_selection_data()
  local valid = M.validate_visual_mode()
  if not valid then
    return nil
  end

  local tree_state, tree_type = M.get_tree_state()
  if not tree_state then
    return nil
  end

  local start_pos, end_pos = M.get_visual_range()

  -- Validate that we have a meaningful range
  if start_pos == 0 or end_pos == 0 then
    return nil
  end

  return {
    tree_state = tree_state,
    tree_type = tree_type,
    start_pos = start_pos,
    end_pos = end_pos,
  }
end

--- Extract files from visual selection in tree buffers
--- @param visual_data table|nil Pre-captured visual selection data
--- @return table files List of file paths
--- @return string|nil error Error message if failed
function M.get_files_from_visual_selection(visual_data)
  -- If we have pre-captured data, use it; otherwise try to get current data
  local tree_state, tree_type, start_pos, end_pos

  if visual_data then
    tree_state = visual_data.tree_state
    tree_type = visual_data.tree_type
    start_pos = visual_data.start_pos
    end_pos = visual_data.end_pos
  else
    local valid, err = M.validate_visual_mode()
    if not valid then
      return {}, err
    end

    tree_state, tree_type = M.get_tree_state()
    if not tree_state then
      return {}, "Not in a supported tree buffer"
    end

    start_pos, end_pos = M.get_visual_range()
  end

  if not tree_state then
    return {}, "Not in a supported tree buffer"
  end

  local files = {}

  if tree_type == "neo-tree" then
    local selected_nodes = {}
    for line = start_pos, end_pos do
      -- Neo-tree's tree:get_node() uses the line number directly (1-based)
      local node = tree_state.tree:get_node(line)
      if node then
        if node.type and node.type ~= "message" then
          table.insert(selected_nodes, node)
        end
      end
    end

    for _, node in ipairs(selected_nodes) do
      if node.type == "file" and node.path and node.path ~= "" then
        local depth = (node.get_depth and node:get_depth()) or 0
        if depth > 1 then
          table.insert(files, node.path)
        end
      elseif node.type == "directory" and node.path and node.path ~= "" then
        local depth = (node.get_depth and node:get_depth()) or 0
        if depth > 1 then
          table.insert(files, node.path)
        end
      end
    end
  elseif tree_type == "nvim-tree" then
    -- For nvim-tree, we need to manually map visual lines to tree nodes
    -- since nvim-tree doesn't have direct line-to-node mapping like neo-tree
    require("claudecode.logger").debug(
      "visual_commands",
      "Processing nvim-tree visual selection from line",
      start_pos,
      "to",
      end_pos
    )

    local nvim_tree_api = tree_state
    local current_buf = vim.api.nvim_get_current_buf()

    -- Get all lines in the visual selection
    local lines = vim.api.nvim_buf_get_lines(current_buf, start_pos - 1, end_pos, false)

    require("claudecode.logger").debug("visual_commands", "Found", #lines, "lines in visual selection")

    -- For each line in the visual selection, try to get the corresponding node
    for i, line_content in ipairs(lines) do
      local line_num = start_pos + i - 1

      -- Set cursor to this line to get the node
      pcall(vim.api.nvim_win_set_cursor, 0, { line_num, 0 })

      -- Get node under cursor for this line
      local node_success, node = pcall(nvim_tree_api.tree.get_node_under_cursor)
      if node_success and node then
        require("claudecode.logger").debug(
          "visual_commands",
          "Line",
          line_num,
          "node type:",
          node.type,
          "path:",
          node.absolute_path
        )

        if node.type == "file" and node.absolute_path and node.absolute_path ~= "" then
          -- Check if it's not a root-level file (basic protection)
          if not string.match(node.absolute_path, "^/[^/]*$") then
            table.insert(files, node.absolute_path)
          end
        elseif node.type == "directory" and node.absolute_path and node.absolute_path ~= "" then
          table.insert(files, node.absolute_path)
        end
      else
        require("claudecode.logger").debug("visual_commands", "No valid node found for line", line_num)
      end
    end

    require("claudecode.logger").debug("visual_commands", "Extracted", #files, "files from nvim-tree visual selection")

    -- Remove duplicates while preserving order
    local seen = {}
    local unique_files = {}
    for _, file_path in ipairs(files) do
      if not seen[file_path] then
        seen[file_path] = true
        table.insert(unique_files, file_path)
      end
    end
    files = unique_files
  elseif tree_type == "oil" then
    local oil = tree_state
    local bufnr = vim.api.nvim_get_current_buf()

    -- Get current directory once
    local dir_ok, current_dir = pcall(oil.get_current_dir, bufnr)
    if dir_ok and current_dir then
      -- Access the process_oil_entry function through a module method
      for line = start_pos, end_pos do
        local entry_ok, entry = pcall(oil.get_entry_on_line, bufnr, line)
        if entry_ok and entry and entry.name then
          -- Skip parent directory entries

        end
      end
    end
  end

  return files, nil
end

return M

if entry.name ~= ".." and entry.name ~= "." then
            local full_path = current_dir .. entry.name
            if entry.type == "file" or entry.type == "link" then
              table.insert(files, full_path)
            elseif entry.type == "directory" then
              table.insert(files, full_path:match("/$") and full_path or full_path .. "/")
            else
              table.insert(files, full_path)
            end
          end

---@brief WebSocket server for Claude Code Neovim integration
local claudecode_main = require("claudecode") -- Added for version access
local logger = require("claudecode.logger")
local tcp_server = require("claudecode.server.tcp")
local tools = require("claudecode.tools.init") -- Added: Require the tools module

local MCP_PROTOCOL_VERSION = "2024-11-05"

local M = {}

---@class ServerState
---@field server table|nil The TCP server instance
---@field port number|nil The port server is running on
---@field auth_token string|nil The authentication token for validating connections
---@field clients table A list of connected clients
---@field handlers table Message handlers by method name
---@field ping_timer table|nil Timer for sending pings
M.state = {
  server = nil,
  port = nil,
  auth_token = nil,
  clients = {},
  handlers = {},
  ping_timer = nil,
}

---@brief Initialize the WebSocket server
---@param config table Configuration options
---@param auth_token string|nil The authentication token for validating connections
---@return boolean success Whether server started successfully
---@return number|string port_or_error Port number or error message
function M.start(config, auth_token)
  if M.state.server then
    return false, "Server already running"
  end

  M.state.auth_token = auth_token

  -- Log authentication state
  if auth_token then
    logger.debug("server", "Starting WebSocket server with authentication enabled")
    logger.debug("server", "Auth token length:", #auth_token)
  else
    logger.debug("server", "Starting WebSocket server WITHOUT authentication (insecure)")
  end

  M.register_handlers()

  tools.setup(M)

  local callbacks = {
    on_message = function(client, message)
      M._handle_message(client, message)
    end,
    on_connect = function(client)
      M.state.clients[client.id] = client

      -- Log connection with auth status
      if M.state.auth_token then
        logger.debug("server", "Authenticated WebSocket client connected:", client.id)
      else
        logger.debug("server", "WebSocket client connected (no auth):", client.id)
      end

      -- Notify main module about new connection for queue processing
      local main_module = require("claudecode")
      if main_module._process_queued_mentions then
        vim.schedule(function()
          main_module._process_queued_mentions()
        end)
      end
    end,
    on_disconnect = function(client, code, reason)
      M.state.clients[client.id] = nil
      logger.debug(
        "server",
        "WebSocket client disconnected:",
        client.id,
        "(code:",
        code,
        ", reason:",
        (reason or "N/A") .. ")"
      )
    end,
    on_error = function(error_msg)
      logger.error("server", "WebSocket server error:", error_msg)
    end,
  }

  local server, error_msg = tcp_server.create_server(config, callbacks, M.state.auth_token)
  if not server then
    return false, error_msg or "Unknown server creation error"
  end

  M.state.server = server
  M.state.port = server.port

  M.state.ping_timer = tcp_server.start_ping_timer(server, 30000) -- Start ping timer to keep connections alive

  return true, server.port
end

---@brief Stop the WebSocket server
---@return boolean success Whether server stopped successfully
---@return string|nil error_message Error message if any
function M.stop()
  if not M.state.server then
    return false, "Server not running"
  end

  if M.state.ping_timer then
    M.state.ping_timer:stop()
    M.state.ping_timer:close()
    M.state.ping_timer = nil
  end

  tcp_server.stop_server(M.state.server)

  -- CRITICAL: Clear global deferred responses to prevent memory leaks and hanging
  if _G.claude_deferred_responses then
    _G.claude_deferred_responses = {}
  end

  M.state.server = nil
  M.state.port = nil
  M.state.auth_token = nil
  M.state.clients = {}

  return true
end

---@brief Handle incoming WebSocket message
---@param client table The client that sent the message
---@param message string The JSON-RPC message
function M._handle_message(client, message)
  local success, parsed = pcall(vim.json.decode, message)
  if not success then
    M.send_response(client, nil, nil, {
      code = -32700,
      message = "Parse error",
      data = "Invalid JSON",
    })
    return
  end

  if type(parsed) ~= "table" or parsed.jsonrpc ~= "2.0" then
    M.send_response(client, parsed.id, nil, {
      code = -32600,
      message = "Invalid Request",
      data = "Not a valid JSON-RPC 2.0 request",
    })
    return
  end

  if parsed.id then
    M._handle_request(client, parsed)
  else
    M._handle_notification(client, parsed)
  end
end

---@brief Handle JSON-RPC request (requires response)
---@param client table The client that sent the request
---@param request table The parsed JSON-RPC request
function M._handle_request(client, request)
  local method = request.method
  local params = request.params or {}
  local id = request.id

  local handler = M.state.handlers[method]
  if not handler then
    M.send_response(client, id, nil, {
      code = -32601,
      message = "Method not found",
      data = "Unknown method: " .. tostring(method),
    })
    return
  end

  local success, result, error_data = pcall(handler, client, params)
  if success then
    -- Check if this is a deferred response (blocking tool)
    if result and result._deferred then
      logger.debug("server", "Handler returned deferred response - storing for later")
      -- Store the request info for later response
      local deferred_info = {
        client = result.client,
        id = id,
        coroutine = result.coroutine,
        method = method,
        params = result.params,
      }
      -- Set up the completion callback
      M._setup_deferred_response(deferred_info)
      return -- Don't send response now
    end

    if error_data then
      M.send_response(client, id, nil, error_data)
    else
      M.send_response(client, id, result, nil)
    end
  else
    M.send_response(client, id, nil, {
      code = -32603,
      message = "Internal error",
      data = tostring(result), -- result contains error message when pcall fails
    })
  end
end

---@brief Set up deferred response handling for blocking tools
---@param deferred_info table Information about the deferred request
-- Note: deferred_responses table removed - using global _G.claude_deferred_responses instead

-- Add a unique module ID to detect reloading
local module_instance_id = math.random(10000, 99999)
logger.debug("server", "Server module loaded with instance ID:", module_instance_id)

-- Note: debug_deferred_table function removed as deferred_responses table is no longer used

function M._setup_deferred_response(deferred_info)
  local co = deferred_info.coroutine

  logger.debug("server", "Setting up deferred response for coroutine:", tostring(co))
  logger.debug("server", "Storage happening in module instance:", module_instance_id)

  -- Create a response sender function that captures the current server instance
  local response_sender = function(result)
    logger.debug("server", "Deferred response triggered for coroutine:", tostring(co))

    if result and result.content then
      -- MCP-compliant response
      M.send_response(deferred_info.client, deferred_info.id, result, nil)
    elseif result and result.error then
      -- Error response
      M.send_response(deferred_info.client, deferred_info.id, nil, result.error)
    else
      -- Fallback error
      M.send_response(deferred_info.client, deferred_info.id, nil, {
        code = -32603,
        message = "Internal error",
        data = "Deferred response completed with unexpected format",
      })
    end
  end

  -- Store the response sender in a global location that won't be affected by module reloading
  if not _G.claude_deferred_responses then
    _G.claude_deferred_responses = {}
  end
  _G.claude_deferred_responses[tostring(co)] = response_sender

  logger.debug("server", "Stored response sender in global table for coroutine:", tostring(co))
end

-- Note: _send_deferred_response is no longer needed
-- Responses are now handled via the global _G.claude_deferred_responses table
-- to avoid module reloading issues

---@brief Handle JSON-RPC notification (no response)
---@param client table The client that sent the notification
---@param notification table The parsed JSON-RPC notification
function M._handle_notification(client, notification)
  local method = notification.method
  local params = notification.params or {}

  local handler = M.state.handlers[method]
  if handler then
    pcall(handler, client, params)
  end
end

---@brief Register message handlers for the server
function M.register_handlers()
  M.state.handlers = {
    ["initialize"] = function(client, params) -- Renamed from mcp.connect
      return {
        protocolVersion = MCP_PROTOCOL_VERSION,
        capabilities = {
          logging = vim.empty_dict(), -- Ensure this is an object {} not an array []
          prompts = { listChanged = true },
          resources = { subscribe = true, listChanged = true },
          tools = { listChanged = true },
        },
        serverInfo = {
          name = "claudecode-neovim",
          version = claudecode_main.version:string(),
        },
      }
    end,

    ["notifications/initialized"] = function(client, params) -- Added handler for initialized notification
    end,

    ["prompts/list"] = function(client, params) -- Added handler for prompts/list
      return {
        prompts = {}, -- This will be encoded as an empty JSON array
      }
    end,

    ["tools/list"] = function(_client, _params)
      return {
        tools = tools.get_tool_list(),
      }
    end,

    ["tools/call"] = function(client, params)
      logger.debug(
        "server",
        "Received tools/call. Tool: ",
        params and params.name,
        " Arguments: ",
        vim.inspect(params and params.arguments)
      )
      local result_or_error_table = tools.handle_invoke(client, params)

      -- Check if this is a deferred response (blocking tool)
      if result_or_error_table and result_or_error_table._deferred then
        logger.debug("server", "Tool is blocking - setting up deferred response")
        -- Return the deferred response directly - _handle_request will process it
        return result_or_error_table
      end

      -- Log the response for debugging
      logger.debug("server", "Response - tools/call", params and params.name .. ":", vim.inspect(result_or_error_table))

      if result_or_error_table.error then
        return nil, result_or_error_table.error
      elseif result_or_error_table.result then
        return result_or_error_table.result, nil
      else
        -- Should not happen if tools.handle_invoke behaves correctly
        return nil,
          {
            code = -32603,
            message = "Internal error",
            data = "Tool handler returned unexpected format",
          }
      end
    end,
  }
end

---@brief Send a message to a client
---@param client table The client to send to
---@param method string The method name
---@param params table|nil The parameters to send
---@return boolean success Whether message was sent successfully
function M.send(client, method, params)
  if not M.state.server then
    return false
  end

  local message = {
    jsonrpc = "2.0",
    method = method,
    params = params or vim.empty_dict(),
  }

  local json_message = vim.json.encode(message)
  tcp_server.send_to_client(M.state.server, client.id, json_message)
  return true
end

---@brief Send a response to a client
---@param client table The client to send to
---@param id number|string|nil The request ID to respond to
---@param result any|nil The result data if successful
---@param error_data table|nil The error data if failed
---@return boolean success Whether response was sent successfully
function M.send_response(client, id, result, error_data)
  if not M.state.server then
    return false
  end

  local response = {
    jsonrpc = "2.0",
    id = id,
  }

  if error_data then
    response.error = error_data
  else
    response.result = result
  end

  local json_response = vim.json.encode(response)
  tcp_server.send_to_client(M.state.server, client.id, json_response)
  return true
end

---@brief Broadcast a message to all connected clients
---@param method string The method name
---@param params table|nil The parameters to send
---@return boolean success Whether broadcast was successful
function M.broadcast(method, params)
  if not M.state.server then
    return false
  end

  local message = {
    jsonrpc = "2.0",
    method = method,
    params = params or vim.empty_dict(),
  }

  local json_message = vim.json.encode(message)
  tcp_server.broadcast(M.state.server, json_message)
  return true
end

---@brief Get server status information
---@return table status Server status information
function M.get_status()
  if not M.state.server then
    return {
      running = false,
      port = nil,
      client_count = 0,
    }
  end

  return {
    running = true,
    port = M.state.port,
    client_count = tcp_server.get_client_count(M.state.server),
    clients = tcp_server.get_clients_info(M.state.server),
  }
end

return M

---@brief TCP server implementation using vim.loop
local client_manager = require("claudecode.server.client")
local utils = require("claudecode.server.utils")

local M = {}

---@class TCPServer
---@field server table The vim.loop TCP server handle
---@field port number The port the server is listening on
---@field auth_token string|nil The authentication token for validating connections
---@field clients table Table of connected clients (client_id -> WebSocketClient)
---@field on_message function Callback for WebSocket messages
---@field on_connect function Callback for new connections
---@field on_disconnect function Callback for client disconnections
---@field on_error fun(err_msg: string) Callback for errors

---@brief Find an available port by attempting to bind
---@param min_port number Minimum port to try
---@param max_port number Maximum port to try
---@return number|nil port Available port number, or nil if none found
function M.find_available_port(min_port, max_port)
  if min_port > max_port then
    return nil -- Or handle error appropriately
  end

  local ports = {}
  for i = min_port, max_port do
    table.insert(ports, i)
  end

  -- Shuffle the ports
  utils.shuffle_array(ports)

  -- Try to bind to a port from the shuffled list
  for _, port in ipairs(ports) do
    local test_server = vim.loop.new_tcp()
    local success = test_server:bind("127.0.0.1", port)
    test_server:close()

    if success then
      return port
    end
  end

  return nil
end

---@brief Create and start a TCP server
---@param config table Server configuration
---@param callbacks table Callback functions
---@param auth_token string|nil Authentication token for validating connections
---@return TCPServer|nil server The server object, or nil on error
---@return string|nil error Error message if failed
function M.create_server(config, callbacks, auth_token)
  local port = M.find_available_port(config.port_range.min, config.port_range.max)
  if not port then
    return nil, "No available ports in range " .. config.port_range.min .. "-" .. config.port_range.max
  end

  local tcp_server = vim.loop.new_tcp()
  if not tcp_server then
    return nil, "Failed to create TCP server"
  end

  -- Create server object
  local server = {
    server = tcp_server,
    port = port,
    auth_token = auth_token,
    clients = {},
    on_message = callbacks.on_message or function() end,
    on_connect = callbacks.on_connect or function() end,
    on_disconnect = callbacks.on_disconnect or function() end,
    on_error = callbacks.on_error or function() end,
  }

  local bind_success, bind_err = tcp_server:bind("127.0.0.1", port)
  if not bind_success then
    tcp_server:close()
    return nil, "Failed to bind to port " .. port .. ": " .. (bind_err or "unknown error")
  end

  -- Start listening
  local listen_success, listen_err = tcp_server:listen(128, function(err)
    if err then
      callbacks.on_error("Listen error: " .. err)
      return
    end

    M._handle_new_connection(server)
  end)

  if not listen_success then
    tcp_server:close()
    return nil, "Failed to listen on port " .. port .. ": " .. (listen_err or "unknown error")
  end

  return server, nil
end

---@brief Handle a new client connection
---@param server TCPServer The server object
function M._handle_new_connection(server)
  local client_tcp = vim.loop.new_tcp()
  if not client_tcp then
    server.on_error("Failed to create client TCP handle")
    return
  end

  local accept_success, accept_err = server.server:accept(client_tcp)
  if not accept_success then
    server.on_error("Failed to accept connection: " .. (accept_err or "unknown error"))
    client_tcp:close()
    return
  end

  -- Create WebSocket client wrapper
  local client = client_manager.create_client(client_tcp)
  server.clients[client.id] = client

  -- Set up data handler
  client_tcp:read_start(function(err, data)
    if err then
      server.on_error("Client read error: " .. err)
      M._remove_client(server, client)
      return
    end

    if not data then
      -- EOF - client disconnected
      M._remove_client(server, client)
      return
    end

    -- Process incoming data
    client_manager.process_data(client, data, function(cl, message)
      server.on_message(cl, message)
    end, function(cl, code, reason)
      server.on_disconnect(cl, code, reason)
      M._remove_client(server, cl)
    end, function(cl, error_msg)
      server.on_error("Client " .. cl.id .. " error: " .. error_msg)
      M._remove_client(server, cl)
    end, server.auth_token)
  end)

  -- Notify about new connection
  server.on_connect(client)
end

---@brief Remove a client from the server
---@param server TCPServer The server object
---@param client WebSocketClient The client to remove
function M._remove_client(server, client)
  if server.clients[client.id] then
    server.clients[client.id] = nil

    if not client.tcp_handle:is_closing() then
      client.tcp_handle:close()
    end
  end
end

---@brief Send a message to a specific client
---@param server TCPServer The server object
---@param client_id string The client ID
---@param message string The message to send
---@param callback function|nil Optional callback
function M.send_to_client(server, client_id, message, callback)
  local client = server.clients[client_id]
  if not client then
    if callback then
      callback("Client not found: " .. client_id)
    end
    return
  end

  client_manager.send_message(client, message, callback)
end

---@brief Broadcast a message to all connected clients
---@param server TCPServer The server object
---@param message string The message to broadcast
function M.broadcast(server, message)
  for _, client in pairs(server.clients) do
    client_manager.send_message(client, message)
  end
end

---@brief Get the number of connected clients
---@param server TCPServer The server object
---@return number count Number of connected clients
function M.get_client_count(server)
  local count = 0
  for _ in pairs(server.clients) do
    count = count + 1
  end
  return count
end

---@brief Get information about all clients
---@param server TCPServer The server object
---@return table clients Array of client information
function M.get_clients_info(server)
  local clients = {}
  for _, client in pairs(server.clients) do
    table.insert(clients, client_manager.get_client_info(client))
  end
  return clients
end

---@brief Close a specific client connection
---@param server TCPServer The server object
---@param client_id string The client ID
---@param code number|nil Close code
---@param reason string|nil Close reason
function M.close_client(server, client_id, code, reason)
  local client = server.clients[client_id]
  if client then
    client_manager.close_client(client, code, reason)
  end
end

---@brief Stop the TCP server
---@param server TCPServer The server object
function M.stop_server(server)
  -- Close all clients
  for _, client in pairs(server.clients) do
    client_manager.close_client(client, 1001, "Server shutting down")
  end

  -- Clear clients
  server.clients = {}

  -- Close server
  if server.server and not server.server:is_closing() then
    server.server:close()
  end
end

---@brief Start a periodic ping task to keep connections alive
---@param server TCPServer The server object
---@param interval number Ping interval in milliseconds (default: 30000)
---@return table timer The timer handle
function M.start_ping_timer(server, interval)
  interval = interval or 30000 -- 30 seconds

  local timer = vim.loop.new_timer()
  timer:start(interval, interval, function()
    for _, client in pairs(server.clients) do
      if client.state == "connected" then
        -- Check if client is alive
        if client_manager.is_client_alive(client, interval * 2) then
          client_manager.send_ping(client, "ping")
        else
          -- Client appears dead, close it
          server.on_error("Client " .. client.id .. " appears dead, closing")
          client_manager.close_client(client, 1006, "Connection timeout")
          M._remove_client(server, client)
        end
      end
    end
  end)

  return timer
end

return M

---@brief WebSocket client connection management
local frame = require("claudecode.server.frame")
local handshake = require("claudecode.server.handshake")
local logger = require("claudecode.logger")

local M = {}

---@class WebSocketClient
---@field id string Unique client identifier
---@field tcp_handle table The vim.loop TCP handle
---@field state string Connection state: "connecting", "connected", "closing", "closed"
---@field buffer string Incoming data buffer
---@field handshake_complete boolean Whether WebSocket handshake is complete
---@field last_ping number Timestamp of last ping sent
---@field last_pong number Timestamp of last pong received

---@brief Create a new WebSocket client
---@param tcp_handle table The vim.loop TCP handle
---@return WebSocketClient client The client object
function M.create_client(tcp_handle)
  local client_id = tostring(tcp_handle):gsub("userdata: ", "client_")

  local client = {
    id = client_id,
    tcp_handle = tcp_handle,
    state = "connecting",
    buffer = "",
    handshake_complete = false,
    last_ping = 0,
    last_pong = vim.loop.now(),
  }

  return client
end

---@brief Process incoming data for a client
---@param client WebSocketClient The client object
---@param data string The incoming data
---@param on_message function Callback for complete messages: function(client, message_text)
---@param on_close function Callback for client close: function(client, code, reason)
---@param on_error function Callback for errors: function(client, error_msg)
---@param auth_token string|nil Expected authentication token for validation
function M.process_data(client, data, on_message, on_close, on_error, auth_token)
  client.buffer = client.buffer .. data

  if not client.handshake_complete then
    local complete, request, remaining = handshake.extract_http_request(client.buffer)
    if complete then
      logger.debug("client", "Processing WebSocket handshake for client:", client.id)

      -- Log if auth token is required
      if auth_token then
        logger.debug("client", "Authentication required for client:", client.id)
      else
        logger.debug("client", "No authentication required for client:", client.id)
      end

      local success, response_from_handshake, _ = handshake.process_handshake(request, auth_token)

      -- Log authentication results
      if success then
        if auth_token then
          logger.debug("client", "Client authenticated successfully:", client.id)
        else
          logger.debug("client", "Client handshake completed (no auth required):", client.id)
        end
      else
        -- Log specific authentication failure details
        if auth_token and response_from_handshake:find("auth") then
          logger.warn(
            "client",
            "Authentication failed for client "
              .. client.id
              .. ": "
              .. (response_from_handshake:match("Bad WebSocket upgrade request: (.+)") or "unknown auth error")
          )
        else
          logger.warn(
            "client",
            "WebSocket handshake failed for client "
              .. client.id
              .. ": "
              .. (response_from_handshake:match("HTTP/1.1 %d+ (.+)") or "unknown handshake error")
          )
        end
      end

      client.tcp_handle:write(response_from_handshake, function(err)
        if err then
          logger.error("client", "Failed to send handshake response to client " .. client.id .. ": " .. err)
          on_error(client, "Failed to send handshake response: " .. err)
          return
        end

        if success then
          client.handshake_complete = true
          client.state = "connected"
          client.buffer = remaining
          logger.debug("client", "WebSocket connection established for client:", client.id)

          if #client.buffer > 0 then
            M.process_data(client, "", on_message, on_close, on_error, auth_token)
          end
        else
          client.state = "closing"
          logger.debug("client", "Closing connection for client due to failed handshake:", client.id)
          vim.schedule(function()
            client.tcp_handle:close()
          end)
        end
      end)
    end
    return
  end

  while #client.buffer >= 2 do -- Minimum frame size
    local parsed_frame, bytes_consumed = frame.parse_frame(client.buffer)

    if not parsed_frame then
      break
    end

    -- Frame validation is now handled entirely within frame.parse_frame.
    -- If frame.parse_frame returns a frame, it's considered valid.

    client.buffer = client.buffer:sub(bytes_consumed + 1)

    if parsed_frame.opcode == frame.OPCODE.TEXT then
      vim.schedule(function()
        on_message(client, parsed_frame.payload)
      end)
    elseif parsed_frame.opcode == frame.OPCODE.BINARY then
      -- Binary message (treat as text for JSON-RPC)
      vim.schedule(function()
        on_message(client, parsed_frame.payload)
      end)
    elseif parsed_frame.opcode == frame.OPCODE.CLOSE then
      local code = 1000
      local reason = ""

      if #parsed_frame.payload >= 2 then
        local payload = parsed_frame.payload
        code = payload:byte(1) * 256 + payload:byte(2)
        if #payload > 2 then
          reason = payload:sub(3)
        end
      end

      if client.state == "connected" then
        local close_frame = frame.create_close_frame(code, reason)
        client.tcp_handle:write(close_frame)
        client.state = "closing"
      end

      vim.schedule(function()
        on_close(client, code, reason)
      end)
    elseif parsed_frame.opcode == frame.OPCODE.PING then
      local pong_frame = frame.create_pong_frame(parsed_frame.payload)
      client.tcp_handle:write(pong_frame)
    elseif parsed_frame.opcode == frame.OPCODE.PONG then
      client.last_pong = vim.loop.now()
    elseif parsed_frame.opcode == frame.OPCODE.CONTINUATION then
      -- Continuation frame - for simplicity, we don't support fragmentation
      on_error(client, "Fragmented messages not supported")
      M.close_client(client, 1003, "Unsupported data")
    else
      on_error(client, "Unknown WebSocket opcode: " .. parsed_frame.opcode)
      M.close_client(client, 1002, "Protocol error")
    end
  end
end

---@brief Send a text message to a client
---@param client WebSocketClient The client object
---@param message string The message to send
---@param callback function|nil Optional callback: function(err)
function M.send_message(client, message, callback)
  if client.state ~= "connected" then
    if callback then
      callback("Client not connected")
    end
    return
  end

  local text_frame = frame.create_text_frame(message)
  client.tcp_handle:write(text_frame, callback)
end

---@brief Send a ping to a client
---@param client WebSocketClient The client object
---@param data string|nil Optional ping data
function M.send_ping(client, data)
  if client.state ~= "connected" then
    return
  end

  local ping_frame = frame.create_ping_frame(data or "")
  client.tcp_handle:write(ping_frame)
  client.last_ping = vim.loop.now()
end

---@brief Close a client connection
---@param client WebSocketClient The client object
---@param code number|nil Close code (default: 1000)
---@param reason string|nil Close reason
function M.close_client(client, code, reason)
  if client.state == "closed" or client.state == "closing" then
    return
  end

  code = code or 1000
  reason = reason or ""

  if client.handshake_complete then
    local close_frame = frame.create_close_frame(code, reason)
    client.tcp_handle:write(close_frame, function()
      client.state = "closed"
      client.tcp_handle:close()
    end)
  else
    client.state = "closed"
    client.tcp_handle:close()
  end

  client.state = "closing"
end

---@brief Check if a client connection is alive
---@param client WebSocketClient The client object
---@param timeout number Timeout in milliseconds (default: 30000)
---@return boolean alive True if the client is considered alive
function M.is_client_alive(client, timeout)
  timeout = timeout or 30000 -- 30 seconds default

  if client.state ~= "connected" then
    return false
  end

  local now = vim.loop.now()
  return (now - client.last_pong) < timeout
end

---@brief Get client info for debugging
---@param client WebSocketClient The client object
---@return table info Client information
function M.get_client_info(client)
  return {
    id = client.id,
    state = client.state,
    handshake_complete = client.handshake_complete,
    buffer_size = #client.buffer,
    last_ping = client.last_ping,
    last_pong = client.last_pong,
  }
end

return M

---@brief [[
--- Mock WebSocket server implementation for testing.
--- This module provides a minimal implementation of the WebSocket server
--- functionality, suitable for testing or when real WebSocket connections
--- are not available or needed.
---@brief ]]

local M = {}
local tools = require("claudecode.tools.init")

--- Mock server state
M.state = {
  server = nil,
  port = nil,
  clients = {},
  handlers = {},
  messages = {}, -- Store messages for testing
}

--- Find an available port in the given range
---@param min number The minimum port number
---@param _max number The maximum port number
---@return number port The selected port
function M.find_available_port(min, _max)
  -- For mock implementation, just return the minimum port
  -- In a real implementation, this would scan for available ports in the range
  return min
end

--- Start the WebSocket server
---@param config table Configuration options
---@return boolean success Whether the server started successfully
---@return number|string port_or_error The port number or error message
function M.start(config)
  if M.state.server then
    -- Already running
    return false, "Server already running"
  end

  -- Find an available port
  local port = M.find_available_port(config.port_range.min, config.port_range.max)

  if not port then
    return false, "No available ports found"
  end

  -- Store the port in state
  M.state.port = port

  -- Create mock server object
  M.state.server = {
    port = port,
    clients = {},
    on_message = function() end,
    on_connect = function() end,
    on_disconnect = function() end,
  }

  -- Register message handlers
  M.register_handlers()

  return true, port
end

--- Stop the WebSocket server
---@return boolean success Whether the server stopped successfully
---@return string|nil error Error message if failed
function M.stop()
  if not M.state.server then
    -- Not running
    return false, "Server not running"
  end

  -- Reset state
  M.state.server = nil
  M.state.port = nil
  M.state.clients = {}
  M.state.messages = {}

  return true
end

--- Register message handlers
function M.register_handlers()
  -- Default handlers
  M.state.handlers = {
    ["mcp.connect"] = function(_client, _params)
      -- Handle connection handshake
      -- Parameters not used in this mock implementation
      return { result = { message = "Connection established" } }
    end,

    ["mcp.tool.invoke"] = function(client, params)
      -- Handle tool invocation by dispatching to tools implementation
      return tools.handle_invoke(client, params)
    end,
  }
end

--- Add a client to the server
---@param client_id string A unique client identifier
---@return table client The client object
function M.add_client(client_id)
  if not M.state.server then
    error("Server not running")
  end

  local client = {
    id = client_id,
    connected = true,
    messages = {},
  }

  M.state.clients[client_id] = client
  return client
end

--- Remove a client from the server
---@param client_id string The client identifier
---@return boolean success Whether removal was successful
function M.remove_client(client_id)
  if not M.state.server or not M.state.clients[client_id] then
    return false
  end

  M.state.clients[client_id] = nil
  return true
end

--- Send a message to a client
---@param client table|string The client object or ID
---@param method string The method name
---@param params table The parameters to send
---@return boolean success Whether sending was successful
function M.send(client, method, params)
  local client_obj

  if type(client) == "string" then
    client_obj = M.state.clients[client]
  else
    client_obj = client
  end

  if not client_obj then
    return false
  end

  local message = {
    jsonrpc = "2.0",
    method = method,
    params = params,
  }

  -- Store for testing
  table.insert(client_obj.messages, message)
  table.insert(M.state.messages, {
    client = client_obj.id,
    direction = "outbound",
    message = message,
  })

  return true
end

--- Send a response to a client
---@param client table|string The client object or ID
---@param id string The message ID
---@param result table|nil The result data
---@param error table|nil The error data
---@return boolean success Whether sending was successful
function M.send_response(client, id, result, error)
  local client_obj

  if type(client) == "string" then
    client_obj = M.state.clients[client]
  else
    client_obj = client
  end

  if not client_obj then
    return false
  end

  local response = {
    jsonrpc = "2.0",
    id = id,
  }

  if error then
    response.error = error
  else
    response.result = result
  end

  -- Store for testing
  table.insert(client_obj.messages, response)
  table.insert(M.state.messages, {
    client = client_obj.id,
    direction = "outbound",
    message = response,
  })

  return true
end

--- Broadcast a message to all connected clients
---@param method string The method name
---@param params table The parameters to send
---@return boolean success Whether broadcasting was successful
function M.broadcast(method, params)
  local success = true

  for client_id, _ in pairs(M.state.clients) do
    local send_success = M.send(client_id, method, params)
    success = success and send_success
  end

  return success
end

--- Simulate receiving a message from a client
---@param client_id string The client ID
---@param message table The message to process
---@return table|nil response The response if any
function M.simulate_message(client_id, message)
  local client = M.state.clients[client_id]

  if not client then
    return nil
  end

  -- Store the message
  table.insert(M.state.messages, {
    client = client_id,
    direction = "inbound",
    message = message,
  })

  -- Process the message
  if message.method and M.state.handlers[message.method] then
    local handler = M.state.handlers[message.method]
    local response = handler(client, message.params)

    if message.id and response then
      -- If the message had an ID, this is a request and needs a response
      M.send_response(client, message.id, response.result, response.error)
      return response
    end
  end

  return nil
end

--- Clear test messages
function M.clear_messages()
  M.state.messages = {}

  for _, client in pairs(M.state.clients) do
    client.messages = {}
  end
end

return M

---@brief Utility functions for WebSocket server implementation
local M = {}

-- Lua 5.1 compatible bitwise operations (arithmetic emulation).
local function band(a, b)
  local result = 0
  local bitval = 1
  while a > 0 and b > 0 do
    if a % 2 == 1 and b % 2 == 1 then
      result = result + bitval
    end
    bitval = bitval * 2
    a = math.floor(a / 2)
    b = math.floor(b / 2)
  end
  return result
end

local function bor(a, b)
  local result = 0
  local bitval = 1
  while a > 0 or b > 0 do
    if a % 2 == 1 or b % 2 == 1 then
      result = result + bitval
    end
    bitval = bitval * 2
    a = math.floor(a / 2)
    b = math.floor(b / 2)
  end
  return result
end

local function bxor(a, b)
  local result = 0
  local bitval = 1
  while a > 0 or b > 0 do
    if (a % 2) ~= (b % 2) then
      result = result + bitval
    end
    bitval = bitval * 2
    a = math.floor(a / 2)
    b = math.floor(b / 2)
  end
  return result
end

local function bnot(a)
  return bxor(a, 0xFFFFFFFF)
end

local function lshift(value, amount)
  local shifted_val = value * (2 ^ amount)
  return shifted_val % (2 ^ 32)
end

local function rshift(value, amount)
  return math.floor(value / (2 ^ amount))
end

local function rotleft(value, amount)
  local mask = 0xFFFFFFFF
  value = band(value, mask)
  local part1 = lshift(value, amount)
  local part2 = rshift(value, 32 - amount)
  return band(bor(part1, part2), mask)
end

local function add32(a, b)
  local sum = a + b
  return band(sum, 0xFFFFFFFF)
end

---@brief Generate a random, spec-compliant WebSocket key.
---@return string key Base64 encoded 16-byte random nonce.
function M.generate_websocket_key()
  local random_bytes = {}
  for _ = 1, 16 do
    random_bytes[#random_bytes + 1] = string.char(math.random(0, 255))
  end
  return M.base64_encode(table.concat(random_bytes))
end

---@brief Base64 encode a string
---@param data string The data to encode
---@return string encoded The base64 encoded string
function M.base64_encode(data)
  local chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
  local result = {}
  local padding = ""

  local pad_len = 3 - (#data % 3)
  if pad_len ~= 3 then
    data = data .. string.rep("\0", pad_len)
    padding = string.rep("=", pad_len)
  end

  for i = 1, #data, 3 do
    local a, b, c = data:byte(i, i + 2)
    local bitmap = a * 65536 + b * 256 + c

    -- Use table for efficient string building
    result[#result + 1] = chars:sub(math.floor(bitmap / 262144) + 1, math.floor(bitmap / 262144) + 1)
    result[#result + 1] = chars:sub(math.floor((bitmap % 262144) / 4096) + 1, math.floor((bitmap % 262144) / 4096) + 1)
    result[#result + 1] = chars:sub(math.floor((bitmap % 4096) / 64) + 1, math.floor((bitmap % 4096) / 64) + 1)
    result[#result + 1] = chars:sub((bitmap % 64) + 1, (bitmap % 64) + 1)
  end

  local encoded = table.concat(result)
  return encoded:sub(1, #encoded - #padding) .. padding
end

---@brief Base64 decode a string
---@param data string The base64 encoded string
---@return string|nil decoded The decoded string, or nil on error (e.g. invalid char)
function M.base64_decode(data)
  local chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
  local lookup = {}
  for i = 1, #chars do
    lookup[chars:sub(i, i)] = i - 1
  end
  lookup["="] = 0

  local result = {}
  local buffer = 0
  local bits = 0

  for i = 1, #data do
    local char = data:sub(i, i)
    local value = lookup[char]

    if value == nil then
      return nil
    end

    if char == "=" then
      break
    end

    buffer = (buffer * 64) + value
    bits = bits + 6

    if bits >= 8 then
      bits = bits - 8
      result[#result + 1] = string.char(rshift(buffer, bits))
      buffer = band(buffer, (lshift(1, bits)) - 1)
    end
  end
  return table.concat(result)
end

---@brief Pure Lua SHA-1 implementation
---@param data string The data to hash
---@return string|nil hash The SHA-1 hash in binary format, or nil on error
function M.sha1(data)
  if type(data) ~= "string" then
    return nil
  end

  -- Validate input data is reasonable size (DOS protection)
  if #data > 10 * 1024 * 1024 then -- 10MB limit
    return nil
  end

  local h0 = 0x67452301
  local h1 = 0xEFCDAB89
  local h2 = 0x98BADCFE
  local h3 = 0x10325476
  local h4 = 0xC3D2E1F0

  local msg = data
  local msg_len = #msg
  local bit_len = msg_len * 8

  msg = msg .. string.char(0x80)

  -- Append 0 <= k < 512 bits '0', where the resulting message length
  -- (in bits) is congruent to 448 (mod 512)
  while (#msg % 64) ~= 56 do
    msg = msg .. string.char(0x00)
  end

  -- Append length as 64-bit big-endian integer
  for i = 7, 0, -1 do
    msg = msg .. string.char(band(rshift(bit_len, i * 8), 0xFF))
  end

  for chunk_start = 1, #msg, 64 do
    local w = {}

    -- Break chunk into sixteen 32-bit big-endian words
    for i = 0, 15 do
      local offset = chunk_start + i * 4
      w[i] = bor(
        bor(bor(lshift(msg:byte(offset), 24), lshift(msg:byte(offset + 1), 16)), lshift(msg:byte(offset + 2), 8)),
        msg:byte(offset + 3)
      )
    end

    -- Extend the sixteen 32-bit words into eighty 32-bit words
    for i = 16, 79 do
      w[i] = rotleft(bxor(bxor(bxor(w[i - 3], w[i - 8]), w[i - 14]), w[i - 16]), 1)
    end

    local a, b, c, d, e = h0, h1, h2, h3, h4

    for i = 0, 79 do
      local f, k
      if i <= 19 then
        f = bor(band(b, c), band(bnot(b), d))
        k = 0x5A827999
      elseif i <= 39 then
        f = bxor(bxor(b, c), d)
        k = 0x6ED9EBA1
      elseif i <= 59 then
        f = bor(bor(band(b, c), band(b, d)), band(c, d))
        k = 0x8F1BBCDC
      else
        f = bxor(bxor(b, c), d)
        k = 0xCA62C1D6
      end

      local temp = add32(add32(add32(add32(rotleft(a, 5), f), e), k), w[i])
      e = d
      d = c
      c = rotleft(b, 30)
      b = a
      a = temp
    end

    h0 = add32(h0, a)
    h1 = add32(h1, b)
    h2 = add32(h2, c)
    h3 = add32(h3, d)
    h4 = add32(h4, e)
  end

  -- Produce the final hash value as a 160-bit (20-byte) binary string
  local result = ""
  for _, h in ipairs({ h0, h1, h2, h3, h4 }) do
    result = result
      .. string.char(band(rshift(h, 24), 0xFF), band(rshift(h, 16), 0xFF), band(rshift(h, 8), 0xFF), band(h, 0xFF))
  end

  return result
end

---@brief Generate WebSocket accept key from client key
---@param client_key string The client's WebSocket-Key header value
---@return string|nil accept_key The WebSocket accept key, or nil on error
function M.generate_accept_key(client_key)
  local magic_string = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

  -- As per RFC 6455, the server concatenates the Sec-WebSocket-Key header value
  -- with a magic string, SHA1s the result, and then Base64 encodes it.
  local combined = client_key .. magic_string
  local hash = M.sha1(combined)
  if not hash then
    return nil
  end

  return M.base64_encode(hash)
end

---@brief Parse HTTP headers from request string
---@param request string The HTTP request string
---@return table headers Table of header name -> value pairs
function M.parse_http_headers(request)
  local headers = {}
  local lines = {}

  for line in request:gmatch("[^\r\n]+") do
    table.insert(lines, line)
  end

  for i = 2, #lines do
    local line = lines[i]
    local name, value = line:match("^([^:]+):%s*(.+)$")
    if name and value then
      headers[name:lower()] = value
    end
  end

  return headers
end

---@brief Check if a string contains valid UTF-8
---@param str string The string to check
---@return boolean valid True if the string is valid UTF-8
function M.is_valid_utf8(str)
  local i = 1
  while i <= #str do
    local byte = str:byte(i)
    local char_len = 1

    if byte >= 0x80 then
      if byte >= 0xF0 then
        char_len = 4
      elseif byte >= 0xE0 then
        char_len = 3
      elseif byte >= 0xC0 then
        char_len = 2
      else
        return false
      end

      for j = 1, char_len - 1 do
        if i + j > #str then
          return false
        end
        local cont_byte = str:byte(i + j)
        if cont_byte < 0x80 or cont_byte >= 0xC0 then
          return false
        end
      end
    end

    i = i + char_len
  end

  return true
end

---@brief Convert a 16-bit number to big-endian bytes
---@param num number The number to convert
---@return string bytes The big-endian byte representation
function M.uint16_to_bytes(num)
  return string.char(math.floor(num / 256), num % 256)
end

---@brief Convert a 64-bit number to big-endian bytes
---@param num number The number to convert
---@return string bytes The big-endian byte representation
function M.uint64_to_bytes(num)
  local bytes = {}
  for i = 8, 1, -1 do
    bytes[i] = num % 256
    num = math.floor(num / 256)
  end
  return string.char(unpack(bytes))
end

---@brief Convert big-endian bytes to a 16-bit number
---@param bytes string The byte string (2 bytes)
---@return number num The converted number
function M.bytes_to_uint16(bytes)
  if #bytes < 2 then
    return 0
  end
  return bytes:byte(1) * 256 + bytes:byte(2)
end

---@brief Convert big-endian bytes to a 64-bit number
---@param bytes string The byte string (8 bytes)
---@return number num The converted number
function M.bytes_to_uint64(bytes)
  if #bytes < 8 then
    return 0
  end

  local num = 0
  for i = 1, 8 do
    num = num * 256 + bytes:byte(i)
  end
  return num
end

---@brief XOR lookup table for faster operations
local xor_table = {}
for i = 0, 255 do
  xor_table[i] = {}
  for j = 0, 255 do
    local result = 0
    local a, b = i, j
    local bit_val = 1

    while a > 0 or b > 0 do
      local a_bit = a % 2
      local b_bit = b % 2

      if a_bit ~= b_bit then
        result = result + bit_val
      end

      a = math.floor(a / 2)
      b = math.floor(b / 2)
      bit_val = bit_val * 2
    end

    xor_table[i][j] = result
  end
end

---@brief Apply XOR mask to payload data
---@param data string The data to mask/unmask
---@param mask string The 4-byte mask
---@return string masked The masked/unmasked data
function M.apply_mask(data, mask)
  local result = {}
  local mask_bytes = { mask:byte(1, 4) }

  for i = 1, #data do
    local mask_idx = ((i - 1) % 4) + 1
    local data_byte = data:byte(i)
    result[i] = string.char(xor_table[data_byte][mask_bytes[mask_idx]])
  end

  return table.concat(result)
end

---@brief Shuffle an array in place using Fisher-Yates algorithm
---@param tbl table The array to shuffle
function M.shuffle_array(tbl)
  math.randomseed(os.time())
  for i = #tbl, 2, -1 do
    local j = math.random(i)
    tbl[i], tbl[j] = tbl[j], tbl[i]
  end
end

return M

---@brief WebSocket frame encoding and decoding (RFC 6455)
local utils = require("claudecode.server.utils")

local M = {}

-- WebSocket opcodes
M.OPCODE = {
  CONTINUATION = 0x0,
  TEXT = 0x1,
  BINARY = 0x2,
  CLOSE = 0x8,
  PING = 0x9,
  PONG = 0xA,
}

---@class WebSocketFrame
---@field fin boolean Final fragment flag
---@field opcode number Frame opcode
---@field masked boolean Mask flag
---@field payload_length number Length of payload data
---@field mask string|nil 4-byte mask (if masked)
---@field payload string Frame payload data

---@brief Parse a WebSocket frame from binary data
---@param data string The binary frame data
---@return WebSocketFrame|nil frame The parsed frame, or nil if incomplete/invalid
---@return number bytes_consumed Number of bytes consumed from input
function M.parse_frame(data)
  if type(data) ~= "string" then
    return nil, 0
  end

  if #data < 2 then
    return nil, 0 -- Need at least 2 bytes for basic header
  end

  local pos = 1
  local byte1 = data:byte(pos)
  local byte2 = data:byte(pos + 1)

  -- Validate byte values
  if not byte1 or not byte2 then
    return nil, 0
  end

  pos = pos + 2

  local fin = math.floor(byte1 / 128) == 1
  local rsv1 = math.floor((byte1 % 128) / 64) == 1
  local rsv2 = math.floor((byte1 % 64) / 32) == 1
  local rsv3 = math.floor((byte1 % 32) / 16) == 1
  local opcode = byte1 % 16

  local masked = math.floor(byte2 / 128) == 1
  local payload_len = byte2 % 128

  -- Validate opcode (RFC 6455 Section 5.2)
  local valid_opcodes = {
    [M.OPCODE.CONTINUATION] = true,
    [M.OPCODE.TEXT] = true,
    [M.OPCODE.BINARY] = true,
    [M.OPCODE.CLOSE] = true,
    [M.OPCODE.PING] = true,
    [M.OPCODE.PONG] = true,
  }

  if not valid_opcodes[opcode] then
    return nil, 0 -- Invalid opcode
  end

  -- Check for reserved bits (must be 0)
  if rsv1 or rsv2 or rsv3 then
    return nil, 0 -- Protocol error
  end

  -- Control frames must have fin=1 and payload ≤ 125 (RFC 6455 Section 5.5)
  if opcode >= M.OPCODE.CLOSE then
    if not fin or payload_len > 125 then
      return nil, 0 -- Protocol violation
    end
  end

  -- Determine actual payload length
  local actual_payload_len = payload_len
  if payload_len == 126 then
    if #data < pos + 1 then
      return nil, 0 -- Need 2 more bytes
    end
    actual_payload_len = utils.bytes_to_uint16(data:sub(pos, pos + 1))
    pos = pos + 2

    -- Allow any valid 16-bit length for compatibility
    -- Note: Technically should be > 125, but some implementations may vary
  elseif payload_len == 127 then
    if #data < pos + 7 then
      return nil, 0 -- Need 8 more bytes
    end
    actual_payload_len = utils.bytes_to_uint64(data:sub(pos, pos + 7))
    pos = pos + 8

    -- Allow any valid 64-bit length for compatibility
    -- Note: Technically should be > 65535, but some implementations may vary

    -- Prevent extremely large payloads (DOS protection)
    if actual_payload_len > 100 * 1024 * 1024 then -- 100MB limit
      return nil, 0
    end
  end

  -- Additional payload length validation
  if actual_payload_len < 0 then
    return nil, 0 -- Invalid negative length
  end

  -- Read mask if present
  local mask = nil
  if masked then
    if #data < pos + 3 then
      return nil, 0 -- Need 4 mask bytes
    end
    mask = data:sub(pos, pos + 3)
    pos = pos + 4
  end

  -- Check if we have enough data for payload
  if #data < pos + actual_payload_len - 1 then
    return nil, 0 -- Incomplete frame
  end

  -- Read payload
  local payload = data:sub(pos, pos + actual_payload_len - 1)
  pos = pos + actual_payload_len

  -- Unmask payload if needed
  if masked and mask then
    payload = utils.apply_mask(payload, mask)
  end

  -- Validate text frame payload is valid UTF-8
  if opcode == M.OPCODE.TEXT and not utils.is_valid_utf8(payload) then
    return nil, 0 -- Invalid UTF-8 in text frame
  end

  -- Basic validation for close frame payload
  if opcode == M.OPCODE.CLOSE and actual_payload_len > 0 then
    if actual_payload_len == 1 then
      return nil, 0 -- Close frame with 1 byte payload is invalid
    end
    -- Allow most close codes for compatibility, only validate UTF-8 for reason text
    if actual_payload_len > 2 then
      local reason = payload:sub(3)
      if not utils.is_valid_utf8(reason) then
        return nil, 0 -- Invalid UTF-8 in close reason
      end
    end
  end

  local frame = {
    fin = fin,
    opcode = opcode,
    masked = masked,
    payload_length = actual_payload_len,
    mask = mask,
    payload = payload,
  }

  return frame, pos - 1
end

---@brief Create a WebSocket frame
---@param opcode number Frame opcode
---@param payload string Frame payload
---@param fin boolean|nil Final fragment flag (default: true)
---@param masked boolean|nil Whether to mask the frame (default: false for server)
---@return string frame_data The encoded frame data
function M.create_frame(opcode, payload, fin, masked)
  fin = fin ~= false -- Default to true
  masked = masked == true -- Default to false

  local frame_data = {}

  -- First byte: FIN + RSV + Opcode
  local byte1 = opcode
  if fin then
    byte1 = byte1 + 128 -- Set FIN bit (0x80)
  end
  table.insert(frame_data, string.char(byte1))

  -- Payload length and mask bit
  local payload_len = #payload
  local byte2 = 0
  if masked then
    byte2 = byte2 + 128 -- Set MASK bit (0x80)
  end

  if payload_len < 126 then
    byte2 = byte2 + payload_len
    table.insert(frame_data, string.char(byte2))
  elseif payload_len < 65536 then
    byte2 = byte2 + 126
    table.insert(frame_data, string.char(byte2))
    table.insert(frame_data, utils.uint16_to_bytes(payload_len))
  else
    byte2 = byte2 + 127
    table.insert(frame_data, string.char(byte2))
    table.insert(frame_data, utils.uint64_to_bytes(payload_len))
  end

  -- Add mask if needed
  local mask = nil
  if masked then
    -- Generate random 4-byte mask
    mask = string.char(math.random(0, 255), math.random(0, 255), math.random(0, 255), math.random(0, 255))
    table.insert(frame_data, mask)
  end

  -- Add payload (masked if needed)
  if masked and mask then
    payload = utils.apply_mask(payload, mask)
  end
  table.insert(frame_data, payload)

  return table.concat(frame_data)
end

---@brief Create a text frame
---@param text string The text to send
---@param fin boolean|nil Final fragment flag (default: true)
---@return string frame_data The encoded frame data
function M.create_text_frame(text, fin)
  return M.create_frame(M.OPCODE.TEXT, text, fin, false)
end

---@brief Create a binary frame
---@param data string The binary data to send
---@param fin boolean|nil Final fragment flag (default: true)
---@return string frame_data The encoded frame data
function M.create_binary_frame(data, fin)
  return M.create_frame(M.OPCODE.BINARY, data, fin, false)
end

---@brief Create a close frame
---@param code number|nil Close code (default: 1000)
---@param reason string|nil Close reason (default: empty)
---@return string frame_data The encoded frame data
function M.create_close_frame(code, reason)
  code = code or 1000
  reason = reason or ""

  local payload = utils.uint16_to_bytes(code) .. reason
  return M.create_frame(M.OPCODE.CLOSE, payload, true, false)
end

---@brief Create a ping frame
---@param data string|nil Ping data (default: empty)
---@return string frame_data The encoded frame data
function M.create_ping_frame(data)
  data = data or ""
  return M.create_frame(M.OPCODE.PING, data, true, false)
end

---@brief Create a pong frame
---@param data string|nil Pong data (should match ping data)
---@return string frame_data The encoded frame data
function M.create_pong_frame(data)
  data = data or ""
  return M.create_frame(M.OPCODE.PONG, data, true, false)
end

---@brief Check if an opcode is a control frame
---@param opcode number The opcode to check
---@return boolean is_control True if it's a control frame
function M.is_control_frame(opcode)
  return opcode >= 0x8
end

---@brief Validate a WebSocket frame
---@param frame WebSocketFrame The frame to validate
---@return boolean valid True if the frame is valid
---@return string|nil error Error message if invalid
function M.validate_frame(frame)
  -- Control frames must not be fragmented
  if M.is_control_frame(frame.opcode) and not frame.fin then
    return false, "Control frames must not be fragmented"
  end

  -- Control frames must have payload <= 125 bytes
  if M.is_control_frame(frame.opcode) and frame.payload_length > 125 then
    return false, "Control frame payload too large"
  end

  -- Check for valid opcodes
  local valid_opcodes = {
    [M.OPCODE.CONTINUATION] = true,
    [M.OPCODE.TEXT] = true,
    [M.OPCODE.BINARY] = true,
    [M.OPCODE.CLOSE] = true,
    [M.OPCODE.PING] = true,
    [M.OPCODE.PONG] = true,
  }

  if not valid_opcodes[frame.opcode] then
    return false, "Invalid opcode: " .. frame.opcode
  end

  -- Text frames must contain valid UTF-8
  if frame.opcode == M.OPCODE.TEXT and not utils.is_valid_utf8(frame.payload) then
    return false, "Text frame contains invalid UTF-8"
  end

  return true
end

return M

---@brief WebSocket handshake handling (RFC 6455)
local utils = require("claudecode.server.utils")

local M = {}

---@brief Check if an HTTP request is a valid WebSocket upgrade request
---@param request string The HTTP request string
---@param expected_auth_token string|nil Expected authentication token for validation
---@return boolean valid True if it's a valid WebSocket upgrade request
---@return table|string headers_or_error Headers table if valid, error message if not
function M.validate_upgrade_request(request, expected_auth_token)
  local headers = utils.parse_http_headers(request)

  -- Check for required headers
  if not headers["upgrade"] or headers["upgrade"]:lower() ~= "websocket" then
    return false, "Missing or invalid Upgrade header"
  end

  if not headers["connection"] or not headers["connection"]:lower():find("upgrade") then
    return false, "Missing or invalid Connection header"
  end

  if not headers["sec-websocket-key"] then
    return false, "Missing Sec-WebSocket-Key header"
  end

  if not headers["sec-websocket-version"] or headers["sec-websocket-version"] ~= "13" then
    return false, "Missing or unsupported Sec-WebSocket-Version header"
  end

  -- Validate WebSocket key format (should be base64 encoded 16 bytes)
  local key = headers["sec-websocket-key"]
  if #key ~= 24 then -- Base64 encoded 16 bytes = 24 characters
    return false, "Invalid Sec-WebSocket-Key format"
  end

  -- Validate authentication token if required
  if expected_auth_token then
    -- Check if expected_auth_token is valid
    if type(expected_auth_token) ~= "string" or expected_auth_token == "" then
      return false, "Server configuration error: invalid expected authentication token"
    end

    local auth_header = headers["x-claude-code-ide-authorization"]
    if not auth_header then
      return false, "Missing authentication header: x-claude-code-ide-authorization"
    end

    -- Check for empty auth header
    if auth_header == "" then
      return false, "Authentication token too short (min 10 characters)"
    end

    -- Check for suspicious auth header lengths
    if #auth_header > 500 then
      return false, "Authentication token too long (max 500 characters)"
    end

    if #auth_header < 10 then
      return false, "Authentication token too short (min 10 characters)"
    end

    if auth_header ~= expected_auth_token then
      return false, "Invalid authentication token"
    end
  end

  return true, headers
end

---@brief Generate a WebSocket handshake response
---@param client_key string The client's Sec-WebSocket-Key header value
---@param protocol string|nil Optional subprotocol to accept
---@return string|nil response The HTTP response string, or nil on error
function M.create_handshake_response(client_key, protocol)
  local accept_key = utils.generate_accept_key(client_key)
  if not accept_key then
    return nil
  end

  local response_lines = {
    "HTTP/1.1 101 Switching Protocols",
    "Upgrade: websocket",
    "Connection: Upgrade",
    "Sec-WebSocket-Accept: " .. accept_key,
  }

  if protocol then
    table.insert(response_lines, "Sec-WebSocket-Protocol: " .. protocol)
  end

  -- Add empty line to end headers
  table.insert(response_lines, "")
  table.insert(response_lines, "")

  return table.concat(response_lines, "\r\n")
end

---@brief Parse the HTTP request line
---@param request string The HTTP request string
---@return string|nil method The HTTP method (GET, POST, etc.)
---@return string|nil path The request path
---@return string|nil version The HTTP version
function M.parse_request_line(request)
  local first_line = request:match("^([^\r\n]+)")
  if not first_line then
    return nil, nil, nil
  end

  local method, path, version = first_line:match("^(%S+)%s+(%S+)%s+(%S+)$")
  return method, path, version
end

---@brief Check if the request is for the WebSocket endpoint
---@param request string The HTTP request string
---@return boolean valid True if the request is for a valid WebSocket endpoint
function M.is_websocket_endpoint(request)
  local method, path, version = M.parse_request_line(request)

  -- Must be GET request
  if method ~= "GET" then
    return false
  end

  -- Must be HTTP/1.1 or later
  if not version or not version:match("^HTTP/1%.1") then
    return false
  end

  -- Accept any path for now (could be made configurable)
  if not path then
    return false
  end

  return true
end

---@brief Create a WebSocket handshake error response
---@param code number HTTP status code
---@param message string Error message
---@return string response The HTTP error response
function M.create_error_response(code, message)
  local status_text = {
    [400] = "Bad Request",
    [404] = "Not Found",
    [426] = "Upgrade Required",
    [500] = "Internal Server Error",
  }

  local status = status_text[code] or "Error"

  local response_lines = {
    "HTTP/1.1 " .. code .. " " .. status,
    "Content-Type: text/plain",
    "Content-Length: " .. #message,
    "Connection: close",
    "",
    message,
  }

  return table.concat(response_lines, "\r\n")
end

---@brief Process a complete WebSocket handshake
---@param request string The HTTP request string
---@param expected_auth_token string|nil Expected authentication token for validation
---@return boolean success True if handshake was successful
---@return string response The HTTP response to send
---@return table|nil headers The parsed headers if successful
function M.process_handshake(request, expected_auth_token)
  -- Check if it's a valid WebSocket endpoint request
  if not M.is_websocket_endpoint(request) then
    local response = M.create_error_response(404, "WebSocket endpoint not found")
    return false, response, nil
  end

  -- Validate the upgrade request
  local is_valid_upgrade, validation_payload = M.validate_upgrade_request(request, expected_auth_token) ---@type boolean, table|string
  if not is_valid_upgrade then
    assert(type(validation_payload) == "string", "validation_payload should be a string on error")
    local error_message = validation_payload
    local response = M.create_error_response(400, "Bad WebSocket upgrade request: " .. error_message)
    return false, response, nil
  end

  -- If is_valid_upgrade is true, validation_payload must be the headers table
  assert(type(validation_payload) == "table", "validation_payload should be a table on success")
  local headers_table = validation_payload

  -- Generate handshake response
  local client_key = headers_table["sec-websocket-key"]
  local protocol = headers_table["sec-websocket-protocol"] -- Optional

  local response = M.create_handshake_response(client_key, protocol)
  if not response then
    local error_response = M.create_error_response(500, "Failed to generate WebSocket handshake response")
    return false, error_response, nil -- error_response is string, nil is for headers
  end

  return true, response, headers_table -- headers_table is 'table', compatible with 'table|nil'
end

---@brief Check if a request buffer contains a complete HTTP request
---@param buffer string The request buffer
---@return boolean complete True if the request is complete
---@return string|nil request The complete request if found
---@return string remaining Any remaining data after the request
function M.extract_http_request(buffer)
  -- Look for the end of HTTP headers (double CRLF)
  local header_end = buffer:find("\r\n\r\n")
  if not header_end then
    return false, nil, buffer
  end

  -- For WebSocket upgrade, there should be no body
  local request = buffer:sub(1, header_end + 3) -- Include the final CRLF
  local remaining = buffer:sub(header_end + 4)

  return true, request, remaining
end

return M

This code segment functions to: Filter and process file system entries by excluding navigation elements, construct full path strings based on entry type, and categorize paths for different resource types while enforcing consistent directory notation.

```lua
[TASK_BEGIN]
          if entry.name ~= ".." then
            local full_path = current_dir .. entry.name
            table.insert(files, full_path)
          end
[TASK_END]
```