--- Snacks.nvim terminal provider for Claude Code.
-- @module claudecode.terminal.snacks

--- @type TerminalProvider
local M = {}

local snacks_available, Snacks = pcall(require, "snacks")
local utils = require("claudecode.utils")
local terminal = nil

--- @return boolean
local function is_available()
  return snacks_available and Snacks and Snacks.terminal
end

--- Setup event handlers for terminal instance
--- @param term_instance table The Snacks terminal instance
--- @param config table Configuration options
local function setup_terminal_events(term_instance, config)
  local logger = require("claudecode.logger")

  -- Handle command completion/exit - only if auto_close is enabled
  if config.auto_close then
    term_instance:on("TermClose", function()
      if vim.v.event.status ~= 0 then
        logger.error("terminal", "Claude exited with code " .. vim.v.event.status .. ".\nCheck for any errors.")
      end

      -- Clean up
      terminal = nil
      vim.schedule(function()
        term_instance:close({ buf = true })
        vim.cmd.checktime()
      end)
    end, { buf = true })
  end

  -- Handle buffer deletion
  term_instance:on("BufWipeout", function()
    logger.debug("terminal", "Terminal buffer wiped")
    terminal = nil
  end, { buf = true })
end

--- Builds Snacks terminal options with focus control
--- @param config table Terminal configuration (split_side, split_width_percentage, etc.)
--- @param env_table table Environment variables to set for the terminal process
--- @param focus boolean|nil Whether to focus the terminal when opened (defaults to true)
--- @return table Snacks terminal options with start_insert/auto_insert controlled by focus parameter
local function build_opts(config, env_table, focus)
  focus = utils.normalize_focus(focus)
  return {
    env = env_table,
    start_insert = focus,
    auto_insert = focus,
    auto_close = false,
    win = {
      position = config.split_side,
      width = config.split_width_percentage,
      height = 0,
      relative = "editor",
    },
  }
end

function M.setup()
  -- No specific setup needed for Snacks provider
end

--- @param cmd_string string
--- @param env_table table
--- @param config table
--- @param focus boolean|nil

function M.close()
  if not is_available() then
    return
  end
  if terminal and terminal:buf_valid() then
    terminal:close()
  end
end

--- Simple toggle: always show/hide terminal regardless of focus
--- @param cmd_string string
--- @param env_table table
--- @param config table
function M.simple_toggle(cmd_string, env_table, config)
  if not is_available() then
    vim.notify("Snacks.nvim terminal provider selected but Snacks.terminal not available.", vim.log.levels.ERROR)
    return
  end

  local logger = require("claudecode.logger")

  -- Check if terminal exists and is visible
  if terminal and terminal:buf_valid() and terminal.win then
    -- Terminal is visible, hide it
    logger.debug("terminal", "Simple toggle: hiding visible terminal")
    terminal:toggle()
  elseif terminal and terminal:buf_valid() and not terminal.win then
    -- Terminal exists but not visible, show it
    logger.debug("terminal", "Simple toggle: showing hidden terminal")
    terminal:toggle()
  else
    -- No terminal exists, create new one
    logger.debug("terminal", "Simple toggle: creating new terminal")
    M.open(cmd_string, env_table, config)
  end
end

--- Smart focus toggle: switches to terminal if not focused, hides if currently focused
--- @param cmd_string string
--- @param env_table table
--- @param config table
function M.focus_toggle(cmd_string, env_table, config)
  if not is_available() then
    vim.notify("Snacks.nvim terminal provider selected but Snacks.terminal not available.", vim.log.levels.ERROR)
    return
  end

  local logger = require("claudecode.logger")

  -- Terminal exists, is valid, but not visible
  if terminal and terminal:buf_valid() and not terminal.win then
    logger.debug("terminal", "Focus toggle: showing hidden terminal")
    terminal:toggle()
  -- Terminal exists, is valid, and is visible
  elseif terminal and terminal:buf_valid() and terminal.win then
    local claude_term_neovim_win_id = terminal.win
    local current_neovim_win_id = vim.api.nvim_get_current_win()

    -- you're IN it
    if claude_term_neovim_win_id == current_neovim_win_id then
      logger.debug("terminal", "Focus toggle: hiding terminal (currently focused)")
      terminal:toggle()
    -- you're NOT in it
    else
      logger.debug("terminal", "Focus toggle: focusing terminal")
      vim.api.nvim_set_current_win(claude_term_neovim_win_id)
      if terminal.buf and vim.api.nvim_buf_is_valid(terminal.buf) then
        if vim.api.nvim_buf_get_option(terminal.buf, "buftype") == "terminal" then
          vim.api.nvim_win_call(claude_term_neovim_win_id, function()
            vim.cmd("startinsert")
          end)
        end
      end
    end
  -- No terminal exists
  else
    logger.debug("terminal", "Focus toggle: creating new terminal")
    M.open(cmd_string, env_table, config)
  end
end

--- Legacy toggle function for backward compatibility (defaults to simple_toggle)
--- @param cmd_string string
--- @param env_table table
--- @param config table
function M.toggle(cmd_string, env_table, config)
  M.simple_toggle(cmd_string, env_table, config)
end

--- @return number|nil
function M.get_active_bufnr()
  if terminal and terminal:buf_valid() and terminal.buf then
    if vim.api.nvim_buf_is_valid(terminal.buf) then
      return terminal.buf
    end
  end
  return nil
end

--- @return boolean
function M.is_available()
  return is_available()
end

-- For testing purposes
--- @return table|nil
function M._get_terminal_for_test()
  return terminal
end

return M

function M.open(cmd_string, env_table, config, focus)
  if not is_available() then
    vim.notify("Snacks.nvim terminal provider selected but Snacks.terminal not available.", vim.log.levels.ERROR)
    return
  end
  focus = utils.normalize_focus(focus)
  if terminal and terminal:buf_valid() then
    if not terminal.win or not vim.api.nvim_win_is_valid(terminal.win) then
      terminal:toggle()
      if focus then
        terminal:focus()
        local term_buf_id = terminal.buf
        if term_buf_id and vim.api.nvim_buf_get_option(term_buf_id, "buftype") == "terminal" then
          if terminal.win and vim.api.nvim_win_is_valid(terminal.win) then
            vim.api.nvim_win_call(terminal.win, function()
              vim.cmd("startinsert")
            end)
          end
        end
      end
    else
      if focus then
        terminal:focus()
        local term_buf_id = terminal.buf
        if term_buf_id and vim.api.nvim_buf_get_option(term_buf_id, "buftype") == "terminal" then
          if terminal.win and vim.api.nvim_win_is_valid(terminal.win) then
            vim.api.nvim_win_call(terminal.win, function()
              vim.cmd("startinsert")
            end)
          end
        end
      end
    end
    return
  end
  local opts = build_opts(config, env_table, focus)
  local term_instance = Snacks.terminal.open(cmd_string, opts)
  if term_instance and term_instance:buf_valid() then
    setup_terminal_events(term_instance, config)
    terminal = term_instance
  else
    terminal = nil
    local logger = require("claudecode.logger")
    local error_details = {}
    if not term_instance then
      table.insert(error_details, "Snacks.terminal.open() returned nil")
    elseif not term_instance:buf_valid() then
      table.insert(error_details, "terminal instance is invalid")
      if term_instance.buf and not vim.api.nvim_buf_is_valid(term_instance.buf) then
        table.insert(error_details, "buffer is invalid")
      end
      if term_instance.win and not vim.api.nvim_win_is_valid(term_instance.win) then
        table.insert(error_details, "window is invalid")
      end
    end
    local context = string.format("cmd='%s', opts=%s", cmd_string, vim.inspect(opts))
    local error_msg = string.format(
      "Failed to open Claude terminal using Snacks. Details: %s. Context: %s",
      table.concat(error_details, ", "),
      context
    )
    vim.notify(error_msg, vim.log.levels.ERROR)
    logger.debug("terminal", error_msg)
  end
end

--- Native Neovim terminal provider for Claude Code.
-- @module claudecode.terminal.native

--- @type TerminalProvider
local M = {}

local logger = require("claudecode.logger")
local utils = require("claudecode.utils")

local bufnr = nil
local winid = nil
local jobid = nil
local tip_shown = false
local config = {}

local function cleanup_state()
  bufnr = nil
  winid = nil
  jobid = nil
end

local function is_valid()
  -- First check if we have a valid buffer
  if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then
    cleanup_state()
    return false
  end

  -- If buffer is valid but window is invalid, try to find a window displaying this buffer
  if not winid or not vim.api.nvim_win_is_valid(winid) then
    -- Search all windows for our terminal buffer
    local windows = vim.api.nvim_list_wins()
    for _, win in ipairs(windows) do
      if vim.api.nvim_win_get_buf(win) == bufnr then
        -- Found a window displaying our terminal buffer, update the tracked window ID
        winid = win
        logger.debug("terminal", "Recovered terminal window ID:", win)
        return true
      end
    end
    -- Buffer exists but no window displays it - this is normal for hidden terminals
    return true -- Buffer is valid even though not visible
  end

  -- Both buffer and window are valid
  return true
end

local function open_terminal(cmd_string, env_table, effective_config, focus)
  focus = utils.normalize_focus(focus)

  if is_valid() then -- Should not happen if called correctly, but as a safeguard
    if focus then
      -- Focus existing terminal: switch to terminal window and enter insert mode
      vim.api.nvim_set_current_win(winid)
      vim.cmd("startinsert")
    end
    -- If focus=false, preserve user context by staying in current window
    return true
  end

  local original_win = vim.api.nvim_get_current_win()
  local width = math.floor(vim.o.columns * effective_config.split_width_percentage)
  local full_height = vim.o.lines
  local placement_modifier

  if effective_config.split_side == "left" then
    placement_modifier = "topleft "
  else
    placement_modifier = "botright "
  end

  vim.cmd(placement_modifier .. width .. "vsplit")
  local new_winid = vim.api.nvim_get_current_win()
  vim.api.nvim_win_set_height(new_winid, full_height)

  vim.api.nvim_win_call(new_winid, function()
    vim.cmd("enew")
  end)

  local term_cmd_arg
  if cmd_string:find(" ", 1, true) then
    term_cmd_arg = vim.split(cmd_string, " ", { plain = true, trimempty = false })
  else
    term_cmd_arg = { cmd_string }
  end

  jobid = vim.fn.termopen(term_cmd_arg, {
    env = env_table,
    on_exit = function(job_id, _, _)
      vim.schedule(function()
        if job_id == jobid then
          logger.debug("terminal", "Terminal process exited, cleaning up")

          -- Ensure we are operating on the correct window and buffer before closing
          local current_winid_for_job = winid
          local current_bufnr_for_job = bufnr

          cleanup_state() -- Clear our managed state first

          if current_winid_for_job and vim.api.nvim_win_is_valid(current_winid_for_job) then
            if current_bufnr_for_job and vim.api.nvim_buf_is_valid(current_bufnr_for_job) then
              -- Optional: Check if the window still holds the same terminal buffer
              if vim.api.nvim_win_get_buf(current_winid_for_job) == current_bufnr_for_job then
                vim.api.nvim_win_close(current_winid_for_job, true)
              end
            else
              -- Buffer is invalid, but window might still be there (e.g. if user changed buffer in term window)
              -- Still try to close the window we tracked.
              vim.api.nvim_win_close(current_winid_for_job, true)
            end
          end
        end
      end)
    end,
  })

  if not jobid or jobid == 0 then
    vim.notify("Failed to open native terminal.", vim.log.levels.ERROR)
    vim.api.nvim_win_close(new_winid, true)
    vim.api.nvim_set_current_win(original_win)
    cleanup_state()
    return false
  end

  winid = new_winid
  bufnr = vim.api.nvim_get_current_buf()
  vim.bo[bufnr].bufhidden = "wipe" -- Wipe buffer when hidden (e.g., window closed)
  -- buftype=terminal is set by termopen

  if focus then
    -- Focus the terminal: switch to terminal window and enter insert mode
    vim.api.nvim_set_current_win(winid)
    vim.cmd("startinsert")
  else
    -- Preserve user context: return to the window they were in before terminal creation
    vim.api.nvim_set_current_win(original_win)
  end

  if config.show_native_term_exit_tip and not tip_shown then
    vim.notify("Native terminal opened. Press Ctrl-\\ Ctrl-N to return to Normal mode.", vim.log.levels.INFO)
    tip_shown = true
  end
  return true
end

local function close_terminal()
  if is_valid() then
    -- Closing the window should trigger on_exit of the job if the process is still running,
    -- which then calls cleanup_state.
    -- If the job already exited, on_exit would have cleaned up.
    -- This direct close is for user-initiated close.
    vim.api.nvim_win_close(winid, true)
    cleanup_state() -- Cleanup after explicit close
  end
end

local function focus_terminal()
  if is_valid() then
    vim.api.nvim_set_current_win(winid)
    vim.cmd("startinsert")
  end
end

local function is_terminal_visible()
  -- Check if our terminal buffer exists and is displayed in any window
  if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then
    return false
  end

  local windows = vim.api.nvim_list_wins()
  for _, win in ipairs(windows) do
    if vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == bufnr then
      -- Update our tracked window ID if we find the buffer in a different window
      winid = win
      return true
    end
  end

  -- Buffer exists but no window displays it
  winid = nil
  return false
end

local function hide_terminal()
  -- Hide the terminal window but keep the buffer and job alive
  if bufnr and vim.api.nvim_buf_is_valid(bufnr) and winid and vim.api.nvim_win_is_valid(winid) then
    -- Set buffer to hide instead of being wiped when window closes
    vim.api.nvim_buf_set_option(bufnr, "bufhidden", "hide")

    -- Close the window - this preserves the buffer and job
    vim.api.nvim_win_close(winid, false)
    winid = nil -- Clear window reference

    logger.debug("terminal", "Terminal window hidden, process preserved")
  end
end

local function show_hidden_terminal(effective_config, focus)
  -- Show an existing hidden terminal buffer in a new window
  if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then
    return false
  end

  -- Check if it's already visible
  if is_terminal_visible() then
    if focus then
      focus_terminal()
    end
    return true
  end

  local original_win = vim.api.nvim_get_current_win()

  -- Create a new window for the existing buffer
  local width = math.floor(vim.o.columns * effective_config.split_width_percentage)
  local full_height = vim.o.lines
  local placement_modifier

  if effective_config.split_side == "left" then
    placement_modifier = "topleft "
  else
    placement_modifier = "botright "
  end

  vim.cmd(placement_modifier .. width .. "vsplit")
  local new_winid = vim.api.nvim_get_current_win()
  vim.api.nvim_win_set_height(new_winid, full_height)

  -- Set the existing buffer in the new window
  vim.api.nvim_win_set_buf(new_winid, bufnr)
  winid = new_winid

  if focus then
    -- Focus the terminal: switch to terminal window and enter insert mode
    vim.api.nvim_set_current_win(winid)
    vim.cmd("startinsert")
  else
    -- Preserve user context: return to the window they were in before showing terminal
    vim.api.nvim_set_current_win(original_win)
  end

  logger.debug("terminal", "Showed hidden terminal in new window")
  return true
end

local function find_existing_claude_terminal()
  local buffers = vim.api.nvim_list_bufs()
  for _, buf in ipairs(buffers) do
    if vim.api.nvim_buf_is_valid(buf) and vim.api.nvim_buf_get_option(buf, "buftype") == "terminal" then
      -- Check if this is a Claude Code terminal by examining the buffer name or terminal job
      local buf_name = vim.api.nvim_buf_get_name(buf)
      -- Terminal buffers often have names like "term://..." that include the command
      if buf_name:match("claude") then
        -- Additional check: see if there's a window displaying this buffer
        local windows = vim.api.nvim_list_wins()
        for _, win in ipairs(windows) do
          if vim.api.nvim_win_get_buf(win) == buf then
            logger.debug("terminal", "Found existing Claude terminal in buffer", buf, "window", win)
            return buf, win
          end
        end
      end
    end
  end
  return nil, nil
end

--- @param term_config table
function M.setup(term_config)
  config = term_config or {}
end

--- @param cmd_string string
--- @param env_table table
--- @param effective_config table
--- @param focus boolean|nil
function M.open(cmd_string, env_table, effective_config, focus)
  focus = utils.normalize_focus(focus)

  if is_valid() then
    -- Check if terminal exists but is hidden (no window)
    if not winid or not vim.api.nvim_win_is_valid(winid) then
      -- Terminal is hidden, show it by calling show_hidden_terminal
      show_hidden_terminal(effective_config, focus)
    else
      -- Terminal is already visible
      if focus then
        focus_terminal()
      end
    end
  else
    -- Check if there's an existing Claude terminal we lost track of
    local existing_buf, existing_win = find_existing_claude_terminal()
    if existing_buf and existing_win then
      -- Recover the existing terminal
      bufnr = existing_buf
      winid = existing_win
      -- Note: We can't recover the job ID easily, but it's less critical
      logger.debug("terminal", "Recovered existing Claude terminal")
      if focus then
        focus_terminal() -- Focus recovered terminal
      end
      -- If focus=false, preserve user context by staying in current window
    else
      if not open_terminal(cmd_string, env_table, effective_config, focus) then
        vim.notify("Failed to open Claude terminal using native fallback.", vim.log.levels.ERROR)
      end
    end
  end
end

function M.close()
  close_terminal()
end

--- Simple toggle: always show/hide terminal regardless of focus
--- @param cmd_string string
--- @param env_table table
--- @param effective_config table
function M.simple_toggle(cmd_string, env_table, effective_config)
  -- Check if we have a valid terminal buffer (process running)
  local has_buffer = bufnr and vim.api.nvim_buf_is_valid(bufnr)
  local is_visible = has_buffer and is_terminal_visible()

  if is_visible then
    -- Terminal is visible, hide it (but keep process running)
    hide_terminal()
  else
    -- Terminal is not visible
    if has_buffer then
      -- Terminal process exists but is hidden, show it
      if show_hidden_terminal(effective_config, true) then
        logger.debug("terminal", "Showing hidden terminal")
      else
        logger.error("terminal", "Failed to show hidden terminal")
      end
    else
      -- No terminal process exists, check if there's an existing one we lost track of
      local existing_buf, existing_win = find_existing_claude_terminal()
      if existing_buf and existing_win then
        -- Recover the existing terminal
        bufnr = existing_buf
        winid = existing_win
        logger.debug("terminal", "Recovered existing Claude terminal")
        focus_terminal()
      else
        -- No existing terminal found, create a new one
        if not open_terminal(cmd_string, env_table, effective_config) then
          vim.notify("Failed to open Claude terminal using native fallback (simple_toggle).", vim.log.levels.ERROR)
        end
      end
    end
  end
end

--- Smart focus toggle: switches to terminal if not focused, hides if currently focused
--- @param cmd_string string
--- @param env_table table
--- @param effective_config table
function M.focus_toggle(cmd_string, env_table, effective_config)
  -- Check if we have a valid terminal buffer (process running)
  local has_buffer = bufnr and vim.api.nvim_buf_is_valid(bufnr)
  local is_visible = has_buffer and is_terminal_visible()

  if has_buffer then
    -- Terminal process exists
    if is_visible then
      -- Terminal is visible - check if we're currently in it
      local current_win_id = vim.api.nvim_get_current_win()
      if winid == current_win_id then
        -- We're in the terminal window, hide it (but keep process running)
        hide_terminal()
      else
        -- Terminal is visible but we're not in it, focus it
        focus_terminal()
      end
    else
      -- Terminal process exists but is hidden, show it
      if show_hidden_terminal(effective_config, true) then
        logger.debug("terminal", "Showing hidden terminal")
      else
        logger.error("terminal", "Failed to show hidden terminal")
      end
    end
  else
    -- No terminal process exists, check if there's an existing one we lost track of
    local existing_buf, existing_win = find_existing_claude_terminal()
    if existing_buf and existing_win then
      -- Recover the existing terminal
      bufnr = existing_buf
      winid = existing_win
      logger.debug("terminal", "Recovered existing Claude terminal")

      -- Check if we're currently in this recovered terminal
      local current_win_id = vim.api.nvim_get_current_win()
      if existing_win == current_win_id then
        -- We're in the recovered terminal, hide it
        hide_terminal()
      else
        -- Focus the recovered terminal
        focus_terminal()
      end
    else
      -- No existing terminal found, create a new one
      if not open_terminal(cmd_string, env_table, effective_config) then
        vim.notify("Failed to open Claude terminal using native fallback (focus_toggle).", vim.log.levels.ERROR)
      end
    end
  end
end

--- Legacy toggle function for backward compatibility (defaults to simple_toggle)
--- @param cmd_string string
--- @param env_table table
--- @param effective_config table
function M.toggle(cmd_string, env_table, effective_config)
  M.simple_toggle(cmd_string, env_table, effective_config)
end

--- @return number|nil
function M.get_active_bufnr()
  if is_valid() then
    return bufnr
  end
  return nil
end

--- @return boolean
function M.is_available()
  return true -- Native provider is always available
end

return M

--- Module to manage a dedicated vertical split terminal for Claude Code.
-- Supports Snacks.nvim or a native Neovim terminal fallback.
-- @module claudecode.terminal

--- @class TerminalProvider
--- @field setup function
--- @field open function
--- @field close function
--- @field toggle function
--- @field simple_toggle function
--- @field focus_toggle function
--- @field get_active_bufnr function
--- @field is_available function
--- @field _get_terminal_for_test function

local M = {}

local claudecode_server_module = require("claudecode.server.init")

local config = {
  split_side = "right",
  split_width_percentage = 0.30,
  provider = "auto",
  show_native_term_exit_tip = true,
  terminal_cmd = nil,
  auto_close = true,
}

-- Lazy load providers
local providers = {}

--- Loads a terminal provider module
--- @param provider_name string The name of the provider to load
--- @return TerminalProvider|nil provider The provider module, or nil if loading failed
local function load_provider(provider_name)
  if not providers[provider_name] then
    local ok, provider = pcall(require, "claudecode.terminal." .. provider_name)
    if ok then
      providers[provider_name] = provider
    else
      return nil
    end
  end
  return providers[provider_name]
end

--- Gets the effective terminal provider, guaranteed to return a valid provider
--- Falls back to native provider if configured provider is unavailable
--- @return TerminalProvider provider The terminal provider module (never nil)
local function get_provider()
  local logger = require("claudecode.logger")

  if config.provider == "auto" then
    -- Try snacks first, then fallback to native silently
    local snacks_provider = load_provider("snacks")
    if snacks_provider and snacks_provider.is_available() then
      return snacks_provider
    end
    -- Fall through to native provider
  elseif config.provider == "snacks" then
    local snacks_provider = load_provider("snacks")
    if snacks_provider and snacks_provider.is_available() then
      return snacks_provider
    else
      logger.warn("terminal", "'snacks' provider configured, but Snacks.nvim not available. Falling back to 'native'.")
    end
  elseif config.provider == "native" then
    -- noop, will use native provider as default below
    logger.debug("terminal", "Using native terminal provider")
  else
    logger.warn("terminal", "Invalid provider configured: " .. tostring(config.provider) .. ". Defaulting to 'native'.")
  end

  local native_provider = load_provider("native")
  if not native_provider then
    error("ClaudeCode: Critical error - native terminal provider failed to load")
  end
  return native_provider
end

--- Builds the effective terminal configuration by merging defaults with overrides
--- @param opts_override table|nil Optional overrides for terminal appearance
--- @return table The effective terminal configuration
local function build_config(opts_override)
  local effective_config = vim.deepcopy(config)
  if type(opts_override) == "table" then
    local validators = {
      split_side = function(val)
        return val == "left" or val == "right"
      end,
      split_width_percentage = function(val)
        return type(val) == "number" and val > 0 and val < 1
      end,
    }
    for key, val in pairs(opts_override) do
      if effective_config[key] ~= nil and validators[key] and validators[key](val) then
        effective_config[key] = val
      end
    end
  end
  return {
    split_side = effective_config.split_side,
    split_width_percentage = effective_config.split_width_percentage,
    auto_close = effective_config.auto_close,
  }
end

--- Checks if a terminal buffer is currently visible in any window
--- @param bufnr number|nil The buffer number to check
--- @return boolean True if the buffer is visible in any window, false otherwise
local function is_terminal_visible(bufnr)
  if not bufnr then
    return false
  end

  local bufinfo = vim.fn.getbufinfo(bufnr)
  return bufinfo and #bufinfo > 0 and #bufinfo[1].windows > 0
end

--- Gets the claude command string and necessary environment variables
--- @param cmd_args string|nil Optional arguments to append to the command
--- @return string cmd_string The command string
--- @return table env_table The environment variables table
local function get_claude_command_and_env(cmd_args)
  -- Inline get_claude_command logic
  local cmd_from_config = config.terminal_cmd
  local base_cmd
  if not cmd_from_config or cmd_from_config == "" then
    base_cmd = "claude" -- Default if not configured
  else
    base_cmd = cmd_from_config
  end

  local cmd_string
  if cmd_args and cmd_args ~= "" then
    cmd_string = base_cmd .. " " .. cmd_args
  else
    cmd_string = base_cmd
  end

  local sse_port_value = claudecode_server_module.state.port
  local env_table = {
    ENABLE_IDE_INTEGRATION = "true",
    FORCE_CODE_TERMINAL = "true",
  }

  if sse_port_value then
    env_table["CLAUDE_CODE_SSE_PORT"] = tostring(sse_port_value)
  end

  return cmd_string, env_table
end

--- Common helper to open terminal without focus if not already visible
--- @param opts_override table|nil Optional config overrides
--- @param cmd_args string|nil Optional command arguments
--- @return boolean True if terminal was opened or already visible
local function ensure_terminal_visible_no_focus(opts_override, cmd_args)
  local provider = get_provider()
  local active_bufnr = provider.get_active_bufnr()

  if is_terminal_visible(active_bufnr) then
    -- Terminal is already visible, do nothing
    return true
  end

  -- Terminal is not visible, open it without focus
  local effective_config = build_config(opts_override)
  local cmd_string, claude_env_table = get_claude_command_and_env(cmd_args)

  provider.open(cmd_string, claude_env_table, effective_config, false) -- false = don't focus
  return true
end

--- Configures the terminal module.
-- Merges user-provided terminal configuration with defaults and sets the terminal command.
-- @param user_term_config table (optional) Configuration options for the terminal.
-- @field user_term_config.split_side string 'left' or 'right' (default: 'right').
-- @field user_term_config.split_width_percentage number Percentage of screen width (0.0 to 1.0, default: 0.30).
-- @field user_term_config.provider string 'snacks' or 'native' (default: 'snacks').
-- @field user_term_config.show_native_term_exit_tip boolean Show tip for exiting native terminal (default: true).
-- @param p_terminal_cmd string|nil The command to run in the terminal (from main config).
function M.setup(user_term_config, p_terminal_cmd)
  if user_term_config == nil then -- Allow nil, default to empty table silently
    user_term_config = {}
  elseif type(user_term_config) ~= "table" then -- Warn if it's not nil AND not a table
    vim.notify("claudecode.terminal.setup expects a table or nil for user_term_config", vim.log.levels.WARN)
    user_term_config = {}
  end

  if p_terminal_cmd == nil or type(p_terminal_cmd) == "string" then
    config.terminal_cmd = p_terminal_cmd
  else
    vim.notify(
      "claudecode.terminal.setup: Invalid terminal_cmd provided: " .. tostring(p_terminal_cmd) .. ". Using default.",
      vim.log.levels.WARN
    )
    config.terminal_cmd = nil -- Fallback to default behavior
  end

  for k, v in pairs(user_term_config) do
    if config[k] ~= nil and k ~= "terminal_cmd" then -- terminal_cmd is handled above
      if k == "split_side" and (v == "left" or v == "right") then
        config[k] = v
      elseif k == "split_width_percentage" and type(v) == "number" and v > 0 and v < 1 then
        config[k] = v
      elseif k == "provider" and (v == "snacks" or v == "native") then
        config[k] = v
      elseif k == "show_native_term_exit_tip" and type(v) == "boolean" then
        config[k] = v
      elseif k == "auto_close" and type(v) == "boolean" then
        config[k] = v
      else
        vim.notify("claudecode.terminal.setup: Invalid value for " .. k .. ": " .. tostring(v), vim.log.levels.WARN)
      end
    elseif k ~= "terminal_cmd" then -- Avoid warning for terminal_cmd if passed in user_term_config
      vim.notify("claudecode.terminal.setup: Unknown configuration key: " .. k, vim.log.levels.WARN)
    end
  end

  -- Setup providers with config
  get_provider().setup(config)
end

--- Opens or focuses the Claude terminal.
-- @param opts_override table (optional) Overrides for terminal appearance (split_side, split_width_percentage).
-- @param cmd_args string|nil (optional) Arguments to append to the claude command.
function M.open(opts_override, cmd_args)
  local effective_config = build_config(opts_override)
  local cmd_string, claude_env_table = get_claude_command_and_env(cmd_args)

  get_provider().open(cmd_string, claude_env_table, effective_config)
end

--- Closes the managed Claude terminal if it's open and valid.
function M.close()
  get_provider().close()
end

--- Simple toggle: always show/hide the Claude terminal regardless of focus.
-- @param opts_override table (optional) Overrides for terminal appearance (split_side, split_width_percentage).
-- @param cmd_args string|nil (optional) Arguments to append to the claude command.
function M.simple_toggle(opts_override, cmd_args)
  local effective_config = build_config(opts_override)
  local cmd_string, claude_env_table = get_claude_command_and_env(cmd_args)

  get_provider().simple_toggle(cmd_string, claude_env_table, effective_config)
end

--- Smart focus toggle: switches to terminal if not focused, hides if currently focused.
-- @param opts_override table (optional) Overrides for terminal appearance (split_side, split_width_percentage).
-- @param cmd_args string|nil (optional) Arguments to append to the claude command.
function M.focus_toggle(opts_override, cmd_args)
  local effective_config = build_config(opts_override)
  local cmd_string, claude_env_table = get_claude_command_and_env(cmd_args)

  get_provider().focus_toggle(cmd_string, claude_env_table, effective_config)
end

--- Toggle open terminal without focus if not already visible, otherwise do nothing.
-- @param opts_override table (optional) Overrides for terminal appearance (split_side, split_width_percentage).
-- @param cmd_args string|nil (optional) Arguments to append to the claude command.
function M.toggle_open_no_focus(opts_override, cmd_args)
  ensure_terminal_visible_no_focus(opts_override, cmd_args)
end

--- Ensures terminal is visible without changing focus. Creates if necessary, shows if hidden.
-- @param opts_override table (optional) Overrides for terminal appearance (split_side, split_width_percentage).
-- @param cmd_args string|nil (optional) Arguments to append to the claude command.
function M.ensure_visible(opts_override, cmd_args)
  ensure_terminal_visible_no_focus(opts_override, cmd_args)
end

--- Toggles the Claude terminal open or closed (legacy function - use simple_toggle or focus_toggle).
-- @param opts_override table (optional) Overrides for terminal appearance (split_side, split_width_percentage).
-- @param cmd_args string|nil (optional) Arguments to append to the claude command.
function M.toggle(opts_override, cmd_args)
  -- Default to simple toggle for backward compatibility
  M.simple_toggle(opts_override, cmd_args)
end

--- Gets the buffer number of the currently active Claude Code terminal.
-- This checks both Snacks and native fallback terminals.
-- @return number|nil The buffer number if an active terminal is found, otherwise nil.
function M.get_active_terminal_bufnr()
  return get_provider().get_active_bufnr()
end

--- Gets the managed terminal instance for testing purposes.
-- NOTE: This function is intended for use in tests to inspect internal state.
-- The underscore prefix indicates it's not part of the public API for regular use.
-- @return snacks.terminal|nil The managed Snacks terminal instance, or nil.
function M._get_managed_terminal_for_test()
  local snacks_provider = load_provider("snacks")
  if snacks_provider and snacks_provider._get_terminal_for_test then
    return snacks_provider._get_terminal_for_test()
  end
  return nil
end

return M

---@brief [[
--- Claude Code Neovim Integration
--- This plugin integrates Claude Code CLI with Neovim, enabling
--- seamless AI-assisted coding experiences directly in Neovim.
---@brief ]]

--- @module 'claudecode'
local M = {}

local logger = require("claudecode.logger")

--- @class ClaudeCode.Version
--- @field major integer Major version number
--- @field minor integer Minor version number
--- @field patch integer Patch version number
--- @field prerelease string|nil Prerelease identifier (e.g., "alpha", "beta")
--- @field string fun(self: ClaudeCode.Version):string Returns the formatted version string

--- The current version of the plugin.
--- @type ClaudeCode.Version
M.version = {
  major = 0,
  minor = 2,
  patch = 0,
  prerelease = nil,
  string = function(self)
    local version = string.format("%d.%d.%d", self.major, self.minor, self.patch)
    if self.prerelease then
      version = version .. "-" .. self.prerelease
    end
    return version
  end,
}

--- @class ClaudeCode.Config
--- @field port_range {min: integer, max: integer} Port range for WebSocket server.
--- @field auto_start boolean Auto-start WebSocket server on Neovim startup.
--- @field terminal_cmd string|nil Custom terminal command to use when launching Claude.
--- @field log_level "trace"|"debug"|"info"|"warn"|"error" Log level.
--- @field track_selection boolean Enable sending selection updates to Claude.
--- @field visual_demotion_delay_ms number Milliseconds to wait before demoting a visual selection.
--- @field connection_wait_delay number Milliseconds to wait after connection before sending queued @ mentions.
--- @field connection_timeout number Maximum time to wait for Claude Code to connect (milliseconds).
--- @field queue_timeout number Maximum time to keep @ mentions in queue (milliseconds).
--- @field diff_opts { auto_close_on_accept: boolean, show_diff_stats: boolean, vertical_split: boolean, open_in_current_tab: boolean } Options for the diff provider.

--- @type ClaudeCode.Config
local default_config = {
  port_range = { min = 10000, max = 65535 },
  auto_start = true,
  terminal_cmd = nil,
  log_level = "info",
  track_selection = true,
  visual_demotion_delay_ms = 50, -- Reduced from 200ms for better responsiveness in tree navigation
  connection_wait_delay = 200, -- Milliseconds to wait after connection before sending queued @ mentions
  connection_timeout = 10000, -- Maximum time to wait for Claude Code to connect (milliseconds)
  queue_timeout = 5000, -- Maximum time to keep @ mentions in queue (milliseconds)
  diff_opts = {
    auto_close_on_accept = true,
    show_diff_stats = true,
    vertical_split = true,
    open_in_current_tab = false,
  },
}

--- @class ClaudeCode.State
--- @field config ClaudeCode.Config The current plugin configuration.
--- @field server table|nil The WebSocket server instance.
--- @field port number|nil The port the server is running on.
--- @field auth_token string|nil The authentication token for the current session.
--- @field initialized boolean Whether the plugin has been initialized.
--- @field queued_mentions table[] Array of queued @ mentions waiting for connection.
--- @field connection_timer table|nil Timer for connection timeout.

--- @type ClaudeCode.State
M.state = {
  config = vim.deepcopy(default_config),
  server = nil,
  port = nil,
  auth_token = nil,
  initialized = false,
  queued_mentions = {},
  connection_timer = nil,
}

---@alias ClaudeCode.TerminalOpts { \
---  split_side?: "left"|"right", \
---  split_width_percentage?: number, \
---  provider?: "auto"|"snacks"|"native", \
---  show_native_term_exit_tip?: boolean }
---
---@alias ClaudeCode.SetupOpts { \
---  terminal?: ClaudeCode.TerminalOpts }

---@brief Check if Claude Code is connected to WebSocket server
---@return boolean connected Whether Claude Code has active connections
function M.is_claude_connected()
  if not M.state.server then
    return false
  end

  local server_module = require("claudecode.server.init")
  local status = server_module.get_status()
  return status.running and status.client_count > 0
end

---@brief Clear the @ mention queue and stop timers
local function clear_mention_queue()
  if #M.state.queued_mentions > 0 then
    logger.debug("queue", "Clearing " .. #M.state.queued_mentions .. " queued @ mentions")
  end

  M.state.queued_mentions = {}

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

---@brief Add @ mention to queue for later sending
---@param mention_data table The @ mention data to queue
local function queue_at_mention(mention_data)
  mention_data.timestamp = vim.loop.now()
  table.insert(M.state.queued_mentions, mention_data)

  logger.debug("queue", "Queued @ mention: " .. vim.inspect(mention_data))

  -- Start connection timer if not already running
  if not M.state.connection_timer then
    M.state.connection_timer = vim.loop.new_timer()
    M.state.connection_timer:start(M.state.config.connection_timeout, 0, function()
      vim.schedule(function()
        if #M.state.queued_mentions > 0 then
          logger.error("queue", "Connection timeout - clearing " .. #M.state.queued_mentions .. " queued @ mentions")
          clear_mention_queue()
        end
      end)
    end)
  end
end

---@brief Process queued @ mentions after connection established
function M._process_queued_mentions()
  if #M.state.queued_mentions == 0 then
    return
  end

  logger.debug("queue", "Processing " .. #M.state.queued_mentions .. " queued @ mentions")

  -- Stop connection timer
  if M.state.connection_timer then
    M.state.connection_timer:stop()
    M.state.connection_timer:close()
    M.state.connection_timer = nil
  end

  -- Wait for connection_wait_delay before sending
  vim.defer_fn(function()
    local mentions_to_send = vim.deepcopy(M.state.queued_mentions)
    M.state.queued_mentions = {} -- Clear queue

    if #mentions_to_send == 0 then
      return
    end

    -- Ensure terminal is visible when processing queued mentions
    local terminal = require("claudecode.terminal")
    terminal.ensure_visible()

    local success_count = 0
    local total_count = #mentions_to_send
    local delay = 10 -- Use same delay as existing batch operations

    local function send_mentions_sequentially(index)
      if index > total_count then
        if success_count > 0 then
          local message = success_count == 1 and "Sent 1 queued @ mention to Claude Code"
            or string.format("Sent %d queued @ mentions to Claude Code", success_count)
          logger.debug("queue", message)
        end
        return
      end

      local mention = mentions_to_send[index]
      local now = vim.loop.now()

      -- Check if mention hasn't expired
      if (now - mention.timestamp) < M.state.config.queue_timeout then
        local success, error_msg = M._broadcast_at_mention(mention.file_path, mention.start_line, mention.end_line)
        if success then
          success_count = success_count + 1
        else
          logger.error("queue", "Failed to send queued @ mention: " .. (error_msg or "unknown error"))
        end
      else
        logger.debug("queue", "Skipped expired @ mention: " .. mention.file_path)
      end

      -- Send next mention with delay
      if index < total_count then
        vim.defer_fn(function()
          send_mentions_sequentially(index + 1)
        end, delay)
      else
        -- Final summary
        if success_count > 0 then
          local message = success_count == 1 and "Sent 1 queued @ mention to Claude Code"
            or string.format("Sent %d queued @ mentions to Claude Code", success_count)
          logger.debug("queue", message)
        end
      end
    end

    send_mentions_sequentially(1)
  end, M.state.config.connection_wait_delay)
end

---@brief Show terminal if Claude is connected and it's not already visible
---@return boolean success Whether terminal was shown or was already visible
function M._ensure_terminal_visible_if_connected()
  if not M.is_claude_connected() then
    return false
  end

  local terminal = require("claudecode.terminal")
  local active_bufnr = terminal.get_active_terminal_bufnr and terminal.get_active_terminal_bufnr()

  if not active_bufnr then
    return false
  end

  local bufinfo = vim.fn.getbufinfo(active_bufnr)[1]
  local is_visible = bufinfo and #bufinfo.windows > 0

  if not is_visible then
    terminal.simple_toggle()
  end

  return true
end

---@brief Send @ mention to Claude Code, handling connection state automatically
---@param file_path string The file path to send
---@param start_line number|nil Start line (0-indexed for Claude)
---@param end_line number|nil End line (0-indexed for Claude)
---@param context string|nil Context for logging
---@return boolean success Whether the operation was successful
---@return string|nil error Error message if failed
function M.send_at_mention(file_path, start_line, end_line, context)
  context = context or "command"

  if not M.state.server then
    logger.error(context, "Claude Code integration is not running")
    return false, "Claude Code integration is not running"
  end

  -- Check if Claude Code is connected
  if M.is_claude_connected() then
    -- Claude is connected, send immediately and ensure terminal is visible
    local success, error_msg = M._broadcast_at_mention(file_path, start_line, end_line)
    if success then
      local terminal = require("claudecode.terminal")
      terminal.ensure_visible()
    end
    return success, error_msg
  else
    -- Claude not connected, queue the mention and launch terminal
    local mention_data = {
      file_path = file_path,
      start_line = start_line,
      end_line = end_line,
      context = context,
    }

    queue_at_mention(mention_data)

    -- Launch terminal with Claude Code
    local terminal = require("claudecode.terminal")
    terminal.open()

    logger.debug(context, "Queued @ mention and launched Claude Code: " .. file_path)

    return true, nil
  end
end

---
--- Set up the plugin with user configuration
---@param opts ClaudeCode.SetupOpts|nil Optional configuration table to override defaults.
---@return table The plugin module
function M.setup(opts)
  opts = opts or {}

  local terminal_opts = nil
  if opts.terminal then
    terminal_opts = opts.terminal
    opts.terminal = nil -- Remove from main opts to avoid polluting M.state.config
  end

  local config = require("claudecode.config")
  M.state.config = config.apply(opts)
  -- vim.g.claudecode_user_config is no longer needed as config values are passed directly.

  logger.setup(M.state.config)

  -- Setup terminal module: always try to call setup to pass terminal_cmd,
  -- even if terminal_opts (for split_side etc.) are not provided.
  local terminal_setup_ok, terminal_module = pcall(require, "claudecode.terminal")
  if terminal_setup_ok then
    -- Guard in case tests or user replace the module with a minimal stub without `setup`.
    if type(terminal_module.setup) == "function" then
      -- terminal_opts might be nil, which the setup function should handle gracefully.
      terminal_module.setup(terminal_opts, M.state.config.terminal_cmd)
    end
  else
    logger.error("init", "Failed to load claudecode.terminal module for setup.")
  end

  local diff = require("claudecode.diff")
  diff.setup(M.state.config)

  if M.state.config.auto_start then
    M.start(false) -- Suppress notification on auto-start
  end

  M._create_commands()

  vim.api.nvim_create_autocmd("VimLeavePre", {
    group = vim.api.nvim_create_augroup("ClaudeCodeShutdown", { clear = true }),
    callback = function()
      if M.state.server then
        M.stop()
      else
        -- Clear queue even if server isn't running
        clear_mention_queue()
      end
    end,
    desc = "Automatically stop Claude Code integration when exiting Neovim",
  })

  M.state.initialized = true
  return M
end

--- Start the Claude Code integration
---@param show_startup_notification? boolean Whether to show a notification upon successful startup (defaults to true)
---@return boolean success Whether the operation was successful
---@return number|string port_or_error The WebSocket port if successful, or error message if failed
function M.start(show_startup_notification)
  if show_startup_notification == nil then
    show_startup_notification = true
  end
  if M.state.server then
    local msg = "Claude Code integration is already running on port " .. tostring(M.state.port)
    logger.warn("init", msg)
    return false, "Already running"
  end

  local server = require("claudecode.server.init")
  local lockfile = require("claudecode.lockfile")

  -- Generate auth token first so we can pass it to the server
  local auth_token
  local auth_success, auth_result = pcall(function()
    return lockfile.generate_auth_token()
  end)

  if not auth_success then
    local error_msg = "Failed to generate authentication token: " .. (auth_result or "unknown error")
    logger.error("init", error_msg)
    return false, error_msg
  end

  auth_token = auth_result

  -- Validate the generated auth token
  if not auth_token or type(auth_token) ~= "string" or #auth_token < 10 then
    local error_msg = "Invalid authentication token generated"
    logger.error("init", error_msg)
    return false, error_msg
  end

  local success, result = server.start(M.state.config, auth_token)

  if not success then
    local error_msg = "Failed to start Claude Code server: " .. (result or "unknown error")
    if result and result:find("auth") then
      error_msg = error_msg .. " (authentication related)"
    end
    logger.error("init", error_msg)
    return false, error_msg
  end

  M.state.server = server
  M.state.port = tonumber(result)
  M.state.auth_token = auth_token

  local lock_success, lock_result, returned_auth_token = lockfile.create(M.state.port, auth_token)

  if not lock_success then
    server.stop()
    M.state.server = nil
    M.state.port = nil
    M.state.auth_token = nil

    local error_msg = "Failed to create lock file: " .. (lock_result or "unknown error")
    if lock_result and lock_result:find("auth") then
      error_msg = error_msg .. " (authentication token issue)"
    end
    logger.error("init", error_msg)
    return false, error_msg
  end

  -- Verify that the auth token in the lock file matches what we generated
  if returned_auth_token ~= auth_token then
    server.stop()
    M.state.server = nil
    M.state.port = nil
    M.state.auth_token = nil

    local error_msg = "Authentication token mismatch between server and lock file"
    logger.error("init", error_msg)
    return false, error_msg
  end

  if M.state.config.track_selection then
    local selection = require("claudecode.selection")
    selection.enable(M.state.server, M.state.config.visual_demotion_delay_ms)
  end

  if show_startup_notification then
    logger.info("init", "Claude Code integration started on port " .. tostring(M.state.port))
  end

  return true, M.state.port
end

--- Stop the Claude Code integration
---@return boolean success Whether the operation was successful
---@return string? error Error message if operation failed
function M.stop()
  if not M.state.server then
    logger.warn("init", "Claude Code integration is not running")
    return false, "Not running"
  end

  local lockfile = require("claudecode.lockfile")
  local lock_success, lock_error = lockfile.remove(M.state.port)

  if not lock_success then
    logger.warn("init", "Failed to remove lock file: " .. lock_error)
    -- Continue with shutdown even if lock file removal fails
  end

  if M.state.config.track_selection then
    local selection = require("claudecode.selection")
    selection.disable()
  end

  local success, error = M.state.server.stop()

  if not success then
    logger.error("init", "Failed to stop Claude Code integration: " .. error)
    return false, error
  end

  M.state.server = nil
  M.state.port = nil
  M.state.auth_token = nil

  -- Clear any queued @ mentions when server stops
  clear_mention_queue()

  logger.info("init", "Claude Code integration stopped")

  return true
end

--- Set up user commands
---@private
function M._create_commands()
  vim.api.nvim_create_user_command("ClaudeCodeStart", function()
    M.start()
  end, {
    desc = "Start Claude Code integration",
  })

  vim.api.nvim_create_user_command("ClaudeCodeStop", function()
    M.stop()
  end, {
    desc = "Stop Claude Code integration",
  })

  vim.api.nvim_create_user_command("ClaudeCodeStatus", function()
    if M.state.server and M.state.port then
      logger.info("command", "Claude Code integration is running on port " .. tostring(M.state.port))
    else
      logger.info("command", "Claude Code integration is not running")
    end
  end, {
    desc = "Show Claude Code integration status",
  })

  ---@param file_paths table List of file paths to add
  ---@param options table|nil Optional settings: { delay?: number, show_summary?: boolean, context?: string }
  ---@return number success_count Number of successfully added files
  ---@return number total_count Total number of files attempted
  local function add_paths_to_claude(file_paths, options)
    options = options or {}
    local delay = options.delay or 0
    local show_summary = options.show_summary ~= false
    local context = options.context or "command"

    if not file_paths or #file_paths == 0 then
      return 0, 0
    end

    local success_count = 0
    local total_count = #file_paths

    if delay > 0 then
      local function send_files_sequentially(index)
        if index > total_count then
          if show_summary then
            local message = success_count == 1 and "Added 1 file to Claude context"
              or string.format("Added %d files to Claude context", success_count)
            if total_count > success_count then
              message = message .. string.format(" (%d failed)", total_count - success_count)
            end

            if total_count > success_count then
              if success_count > 0 then
                logger.warn(context, message)
              else
                logger.error(context, message)
              end
            elseif success_count > 0 then
              logger.info(context, message)
            else
              logger.debug(context, message)
            end
          end
          return
        end

        local file_path = file_paths[index]
        local success, error_msg = M.send_at_mention(file_path, nil, nil, context)
        if success then
          success_count = success_count + 1
        else
          logger.error(context, "Failed to add file: " .. file_path .. " - " .. (error_msg or "unknown error"))
        end

        if index < total_count then
          vim.defer_fn(function()
            send_files_sequentially(index + 1)
          end, delay)
        else
          if show_summary then
            local message = success_count == 1 and "Added 1 file to Claude context"
              or string.format("Added %d files to Claude context", success_count)
            if total_count > success_count then
              message = message .. string.format(" (%d failed)", total_count - success_count)
            end

            if total_count > success_count then
              if success_count > 0 then
                logger.warn(context, message)
              else
                logger.error(context, message)
              end
            elseif success_count > 0 then
              logger.info(context, message)
            else
              logger.debug(context, message)
            end
          end
        end
      end

      send_files_sequentially(1)
    else
      for _, file_path in ipairs(file_paths) do
        local success, error_msg = M.send_at_mention(file_path, nil, nil, context)
        if success then
          success_count = success_count + 1
        else
          logger.error(context, "Failed to add file: " .. file_path .. " - " .. (error_msg or "unknown error"))
        end
      end

      if show_summary and success_count > 0 then
        local message = success_count == 1 and "Added 1 file to Claude context"
          or string.format("Added %d files to Claude context", success_count)
        if total_count > success_count then
          message = message .. string.format(" (%d failed)", total_count - success_count)
        end
        logger.debug(context, message)
      end
    end

    return success_count, total_count
  end

  local function handle_send_normal(opts)
    local current_ft = (vim.bo and vim.bo.filetype) or ""
    local current_bufname = (vim.api and vim.api.nvim_buf_get_name and vim.api.nvim_buf_get_name(0)) or ""

    local is_tree_buffer = current_ft == "NvimTree"
      or current_ft == "neo-tree"
      or current_ft == "oil"
      or string.match(current_bufname, "neo%-tree")
      or string.match(current_bufname, "NvimTree")

    if is_tree_buffer then
      local integrations = require("claudecode.integrations")
      local files, error = integrations.get_selected_files_from_tree()

      if error then
        logger.error("command", "ClaudeCodeSend->TreeAdd: " .. error)
        return
      end

      if not files or #files == 0 then
        logger.warn("command", "ClaudeCodeSend->TreeAdd: No files selected")
        return
      end

      add_paths_to_claude(files, { context = "ClaudeCodeSend->TreeAdd" })

      return
    end

    local selection_module_ok, selection_module = pcall(require, "claudecode.selection")
    if selection_module_ok then
      -- Pass range information if available (for :'<,'> commands)
      local line1, line2 = nil, nil
      if opts and opts.range and opts.range > 0 then
        line1, line2 = opts.line1, opts.line2
      end
      local sent_successfully = selection_module.send_at_mention_for_visual_selection(line1, line2)
      if sent_successfully then
        -- Exit any potential visual mode (for consistency)
        pcall(function()
          if vim.api and vim.api.nvim_feedkeys then
            local esc = vim.api.nvim_replace_termcodes("<Esc>", true, false, true)
            vim.api.nvim_feedkeys(esc, "i", true)
          end
        end)
      end
    else
      logger.error("command", "ClaudeCodeSend: Failed to load selection module.")
    end
  end

  local function handle_send_visual(visual_data, _opts)
    -- Try tree file selection first
    if visual_data then
      local visual_commands = require("claudecode.visual_commands")
      local files, error = visual_commands.get_files_from_visual_selection(visual_data)

      if not error and files and #files > 0 then
        local success_count = add_paths_to_claude(files, {
          delay = 10,
          context = "ClaudeCodeSend_visual",
          show_summary = false,
        })
        if success_count > 0 then
          local message = success_count == 1 and "Added 1 file to Claude context from visual selection"
            or string.format("Added %d files to Claude context from visual selection", success_count)
          logger.debug("command", message)
        end
        return
      end
    end

    -- Handle regular text selection using range from visual mode
    local selection_module_ok, selection_module = pcall(require, "claudecode.selection")
    if not selection_module_ok then
      return
    end

    -- Use the marks left by visual mode instead of trying to get current visual selection
    local line1, line2 = vim.fn.line("'<"), vim.fn.line("'>")
    if line1 and line2 and line1 > 0 and line2 > 0 then
      selection_module.send_at_mention_for_visual_selection(line1, line2)
    else
      selection_module.send_at_mention_for_visual_selection()
    end
  end

  local visual_commands = require("claudecode.visual_commands")
  local unified_send_handler = visual_commands.create_visual_command_wrapper(handle_send_normal, handle_send_visual)

  vim.api.nvim_create_user_command("ClaudeCodeSend", unified_send_handler, {
    desc = "Send current visual selection as an at_mention to Claude Code (supports tree visual selection)",
    range = true,
  })

  local function handle_tree_add_normal()
    if not M.state.server then
      logger.error("command", "ClaudeCodeTreeAdd: Claude Code integration is not running.")
      return
    end

    local integrations = require("claudecode.integrations")
    local files, error = integrations.get_selected_files_from_tree()

    if error then
      logger.error("command", "ClaudeCodeTreeAdd: " .. error)
      return
    end

    if not files or #files == 0 then
      logger.warn("command", "ClaudeCodeTreeAdd: No files selected")
      return
    end

    -- Use connection-aware broadcasting for each file
    local success_count = 0
    local total_count = #files

    for _, file_path in ipairs(files) do
      local success, error_msg = M.send_at_mention(file_path, nil, nil, "ClaudeCodeTreeAdd")
      if success then
        success_count = success_count + 1
      else
        logger.error(
          "command",
          "ClaudeCodeTreeAdd: Failed to add file: " .. file_path .. " - " .. (error_msg or "unknown error")
        )
      end
    end

    if success_count == 0 then
      logger.error("command", "ClaudeCodeTreeAdd: Failed to add any files")
    elseif success_count < total_count then
      local message = string.format("Added %d/%d files to Claude context", success_count, total_count)
      logger.debug("command", message)
    else
      local message = success_count == 1 and "Added 1 file to Claude context"
        or string.format("Added %d files to Claude context", success_count)
      logger.debug("command", message)
    end
  end

  local function handle_tree_add_visual(visual_data)
    if not M.state.server then
      logger.error("command", "ClaudeCodeTreeAdd_visual: Claude Code integration is not running.")
      return
    end

    local visual_cmd_module = require("claudecode.visual_commands")
    local files, error = visual_cmd_module.get_files_from_visual_selection(visual_data)

    if error then
      logger.error("command", "ClaudeCodeTreeAdd_visual: " .. error)
      return
    end

    if not files or #files == 0 then
      logger.warn("command", "ClaudeCodeTreeAdd_visual: No files selected in visual range")
      return
    end

    -- Use connection-aware broadcasting for each file
    local success_count = 0
    local total_count = #files

    for _, file_path in ipairs(files) do
      local success, error_msg = M.send_at_mention(file_path, nil, nil, "ClaudeCodeTreeAdd_visual")
      if success then
        success_count = success_count + 1
      else
        logger.error(
          "command",
          "ClaudeCodeTreeAdd_visual: Failed to add file: " .. file_path .. " - " .. (error_msg or "unknown error")
        )
      end
    end

    if success_count > 0 then
      local message = success_count == 1 and "Added 1 file to Claude context from visual selection"
        or string.format("Added %d files to Claude context from visual selection", success_count)
      logger.debug("command", message)

      if success_count < total_count then
        logger.warn("command", string.format("Added %d/%d files from visual selection", success_count, total_count))
      end
    else
      logger.error("command", "ClaudeCodeTreeAdd_visual: Failed to add any files from visual selection")
    end
  end

  local unified_tree_add_handler =
    visual_commands.create_visual_command_wrapper(handle_tree_add_normal, handle_tree_add_visual)

  vim.api.nvim_create_user_command("ClaudeCodeTreeAdd", unified_tree_add_handler, {
    desc = "Add selected file(s) from tree explorer to Claude Code context (supports visual selection)",
  })

  vim.api.nvim_create_user_command("ClaudeCodeAdd", function(opts)
    if not M.state.server then
      logger.error("command", "ClaudeCodeAdd: Claude Code integration is not running.")
      return
    end

    if not opts.args or opts.args == "" then
      logger.error("command", "ClaudeCodeAdd: No file path provided")
      return
    end

    local args = vim.split(opts.args, "%s+")
    local file_path = args[1]
    local start_line = args[2] and tonumber(args[2]) or nil
    local end_line = args[3] and tonumber(args[3]) or nil

    if #args > 3 then
      logger.error(
        "command",
        "ClaudeCodeAdd: Too many arguments. Usage: ClaudeCodeAdd <file-path> [start-line] [end-line]"
      )
      return
    end

    if args[2] and not start_line then
      logger.error("command", "ClaudeCodeAdd: Invalid start line number: " .. args[2])
      return
    end

    if args[3] and not end_line then
      logger.error("command", "ClaudeCodeAdd: Invalid end line number: " .. args[3])
      return
    end

    if start_line and start_line < 1 then
      logger.error("command", "ClaudeCodeAdd: Start line must be positive: " .. start_line)
      return
    end

    if end_line and end_line < 1 then
      logger.error("command", "ClaudeCodeAdd: End line must be positive: " .. end_line)
      return
    end

    if start_line and end_line and start_line > end_line then
      logger.error(
        "command",
        "ClaudeCodeAdd: Start line (" .. start_line .. ") must be <= end line (" .. end_line .. ")"
      )
      return
    end

    file_path = vim.fn.expand(file_path)
    if vim.fn.filereadable(file_path) == 0 and vim.fn.isdirectory(file_path) == 0 then
      logger.error("command", "ClaudeCodeAdd: File or directory does not exist: " .. file_path)
      return
    end

    local claude_start_line = start_line and (start_line - 1) or nil
    local claude_end_line = end_line and (end_line - 1) or nil

    local success, error_msg = M.send_at_mention(file_path, claude_start_line, claude_end_line, "ClaudeCodeAdd")
    if not success then
      logger.error("command", "ClaudeCodeAdd: " .. (error_msg or "Failed to add file"))
    else
      local message = "ClaudeCodeAdd: Successfully added " .. file_path
      if start_line or end_line then
        if start_line and end_line then
          message = message .. " (lines " .. start_line .. "-" .. end_line .. ")"
        elseif start_line then
          message = message .. " (from line " .. start_line .. ")"
        end
      end
      logger.debug("command", message)
    end
  end, {
    nargs = "+",
    complete = "file",
    desc = "Add specified file or directory to Claude Code context with optional line range",
  })

  local terminal_ok, terminal = pcall(require, "claudecode.terminal")
  if terminal_ok then
    vim.api.nvim_create_user_command("ClaudeCode", function(opts)
      local current_mode = vim.fn.mode()
      if current_mode == "v" or current_mode == "V" or current_mode == "\22" then
        vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", true, false, true), "n", false)
      end
      local cmd_args = opts.args and opts.args ~= "" and opts.args or nil
      terminal.simple_toggle({}, cmd_args)
    end, {
      nargs = "*",
      desc = "Toggle the Claude Code terminal window (simple show/hide) with optional arguments",
    })

    vim.api.nvim_create_user_command("ClaudeCodeFocus", function(opts)
      local current_mode = vim.fn.mode()
      if current_mode == "v" or current_mode == "V" or current_mode == "\22" then
        vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", true, false, true), "n", false)
      end
      local cmd_args = opts.args and opts.args ~= "" and opts.args or nil
      terminal.focus_toggle({}, cmd_args)
    end, {
      nargs = "*",
      desc = "Smart focus/toggle Claude Code terminal (switches to terminal if not focused, hides if focused)",
    })

    vim.api.nvim_create_user_command("ClaudeCodeOpen", function(opts)
      local cmd_args = opts.args and opts.args ~= "" and opts.args or nil
      terminal.open({}, cmd_args)
    end, {
      nargs = "*",
      desc = "Open the Claude Code terminal window with optional arguments",
    })

    vim.api.nvim_create_user_command("ClaudeCodeClose", function()
      terminal.close()
    end, {
      desc = "Close the Claude Code terminal window",
    })
  else
    logger.error(
      "init",
      "Terminal module not found. Terminal commands (ClaudeCode, ClaudeCodeOpen, ClaudeCodeClose) not registered."
    )
  end

  -- Diff management commands
  vim.api.nvim_create_user_command("ClaudeCodeDiffAccept", function()
    local diff = require("claudecode.diff")
    diff.accept_current_diff()
  end, {
    desc = "Accept the current diff changes",
  })

  vim.api.nvim_create_user_command("ClaudeCodeDiffDeny", function()
    local diff = require("claudecode.diff")
    diff.deny_current_diff()
  end, {
    desc = "Deny/reject the current diff changes",
  })
end

--- Get version information
---@return table Version information
function M.get_version()
  return {
    version = M.version:string(),
    major = M.version.major,
    minor = M.version.minor,
    patch = M.version.patch,
    prerelease = M.version.prerelease,
  }
end

--- Format file path for at mention (exposed for testing)
---@param file_path string The file path to format
---@return string formatted_path The formatted path
---@return boolean is_directory Whether the path is a directory
function M._format_path_for_at_mention(file_path)
  -- Input validation
  if not file_path or type(file_path) ~= "string" or file_path == "" then
    error("format_path_for_at_mention: file_path must be a non-empty string")
  end

  -- Only check path existence in production (not tests)
  -- This allows tests to work with mock paths while still providing validation in real usage
  if not package.loaded["busted"] then
    if vim.fn.filereadable(file_path) == 0 and vim.fn.isdirectory(file_path) == 0 then
      error("format_path_for_at_mention: path does not exist: " .. file_path)
    end
  end

  local is_directory = vim.fn.isdirectory(file_path) == 1
  local formatted_path = file_path

  if is_directory then
    local cwd = vim.fn.getcwd()
    if string.find(file_path, cwd, 1, true) == 1 then
      local relative_path = string.sub(file_path, #cwd + 2)
      if relative_path ~= "" then
        formatted_path = relative_path
      else
        formatted_path = "./"
      end
    end
    if not string.match(formatted_path, "/$") then
      formatted_path = formatted_path .. "/"
    end
  else
    local cwd = vim.fn.getcwd()
    if string.find(file_path, cwd, 1, true) == 1 then
      local relative_path = string.sub(file_path, #cwd + 2)
      if relative_path ~= "" then
        formatted_path = relative_path
      end
    end
  end

  return formatted_path, is_directory
end

-- Test helper functions (exposed for testing)
function M._broadcast_at_mention(file_path, start_line, end_line)
  if not M.state.server then
    return false, "Claude Code integration is not running"
  end

  -- Safely format the path and handle validation errors
  local formatted_path, is_directory
  local format_success, format_result, is_dir_result = pcall(M._format_path_for_at_mention, file_path)
  if not format_success then
    return false, format_result -- format_result contains the error message
  end
  formatted_path, is_directory = format_result, is_dir_result

  if is_directory and (start_line or end_line) then
    logger.debug("command", "Line numbers ignored for directory: " .. formatted_path)
    start_line = nil
    end_line = nil
  end

  local params = {
    filePath = formatted_path,
    lineStart = start_line,
    lineEnd = end_line,
  }

  local broadcast_success = M.state.server.broadcast("at_mentioned", params)
  if broadcast_success then
    return true, nil
  else
    local error_msg = "Failed to broadcast " .. (is_directory and "directory" or "file") .. " " .. formatted_path
    logger.error("command", error_msg)
    return false, error_msg
  end
end

function M._add_paths_to_claude(file_paths, options)
  options = options or {}
  local delay = options.delay or 0
  local show_summary = options.show_summary ~= false
  local context = options.context or "command"
  local batch_size = options.batch_size or 10
  local max_files = options.max_files or 100

  if not file_paths or #file_paths == 0 then
    return 0, 0
  end

  if #file_paths > max_files then
    logger.warn(context, string.format("Too many files selected (%d), limiting to %d", #file_paths, max_files))
    local limited_paths = {}
    for i = 1, max_files do
      limited_paths[i] = file_paths[i]
    end
    file_paths = limited_paths
  end

  local success_count = 0
  local total_count = #file_paths

  if delay > 0 then
    local function send_batch(start_index)
      if start_index > total_count then
        if show_summary then
          local message = success_count == 1 and "Added 1 file to Claude context"
            or string.format("Added %d files to Claude context", success_count)
          if total_count > success_count then
            message = message .. string.format(" (%d failed)", total_count - success_count)
          end

          if total_count > success_count then
            if success_count > 0 then
              logger.warn(context, message)
            else
              logger.error(context, message)
            end
          elseif success_count > 0 then
            logger.info(context, message)
          else
            logger.debug(context, message)
          end
        end
        return
      end

      -- Process a batch of files
      local end_index = math.min(start_index + batch_size - 1, total_count)
      local batch_success = 0

      for i = start_index, end_index do
        local file_path = file_paths[i]
        local success, error_msg = M._broadcast_at_mention(file_path)
        if success then
          success_count = success_count + 1
          batch_success = batch_success + 1
        else
          logger.error(context, "Failed to add file: " .. file_path .. " - " .. (error_msg or "unknown error"))
        end
      end

      logger.debug(
        context,
        string.format(
          "Processed batch %d-%d: %d/%d successful",
          start_index,
          end_index,
          batch_success,
          end_index - start_index + 1
        )
      )

      if end_index < total_count then
        vim.defer_fn(function()
          send_batch(end_index + 1)
        end, delay)
      else
        if show_summary then
          local message = success_count == 1 and "Added 1 file to Claude context"
            or string.format("Added %d files to Claude context", success_count)
          if total_count > success_count then
            message = message .. string.format(" (%d failed)", total_count - success_count)
          end

          if total_count > success_count then
            if success_count > 0 then
              logger.warn(context, message)
            else
              logger.error(context, message)
            end
          elseif success_count > 0 then
            logger.info(context, message)
          else
            logger.debug(context, message)
          end
        end
      end
    end

    send_batch(1)
  else
    local progress_interval = math.max(1, math.floor(total_count / 10))

    for i, file_path in ipairs(file_paths) do
      local success, error_msg = M._broadcast_at_mention(file_path)
      if success then
        success_count = success_count + 1
      else
        logger.error(context, "Failed to add file: " .. file_path .. " - " .. (error_msg or "unknown error"))
      end

      if total_count > 20 and i % progress_interval == 0 then
        logger.debug(
          context,
          string.format("Progress: %d/%d files processed (%d successful)", i, total_count, success_count)
        )
      end
    end

    if show_summary then
      local message = success_count == 1 and "Added 1 file to Claude context"
        or string.format("Added %d files to Claude context", success_count)
      if total_count > success_count then
        message = message .. string.format(" (%d failed)", total_count - success_count)
      end

      if total_count > success_count then
        if success_count > 0 then
          logger.warn(context, message)
        else
          logger.error(context, message)
        end
      elseif success_count > 0 then
        logger.info(context, message)
      else
        logger.debug(context, message)
      end
    end
  end

  return success_count, total_count
end

return M

--- Diff module for Claude Code Neovim integration.
-- Provides native Neovim diff functionality with MCP-compliant blocking operations and state management.
local M = {}

local logger = require("claudecode.logger")

-- Global state management for active diffs
local active_diffs = {}
local autocmd_group

--- Get or create the autocmd group
local function get_autocmd_group()
  if not autocmd_group then
    autocmd_group = vim.api.nvim_create_augroup("ClaudeCodeMCPDiff", { clear = true })
  end
  return autocmd_group
end

--- Find a suitable main editor window to open diffs in.
-- Excludes terminals, sidebars, and floating windows.
-- @return number|nil Window ID of the main editor window, or nil if not found
local function find_main_editor_window()
  local windows = vim.api.nvim_list_wins()

  for _, win in ipairs(windows) do
    local buf = vim.api.nvim_win_get_buf(win)
    local buftype = vim.api.nvim_buf_get_option(buf, "buftype")
    local filetype = vim.api.nvim_buf_get_option(buf, "filetype")
    local win_config = vim.api.nvim_win_get_config(win)

    -- Check if this is a suitable window
    local is_suitable = true

    -- Skip floating windows
    if win_config.relative and win_config.relative ~= "" then
      is_suitable = false
    end

    -- Skip special buffer types
    if is_suitable and (buftype == "terminal" or buftype == "prompt") then
      is_suitable = false
    end

    -- Skip known sidebar filetypes and ClaudeCode terminal
    if
      is_suitable
      and (
        filetype == "neo-tree"
        or filetype == "neo-tree-popup"
        or filetype == "ClaudeCode"
        or filetype == "NvimTree"
        or filetype == "oil"
        or filetype == "aerial"
        or filetype == "tagbar"
      )
    then
      is_suitable = false
    end

    -- This looks like a main editor window
    if is_suitable then
      return win
    end
  end

  return nil
end

--- Setup the diff module
-- @param user_diff_config table|nil Reserved for future use
function M.setup(user_diff_config)
  -- Currently no configuration needed for native diff
  -- Parameter kept for API compatibility
end

--- Open a diff view between two files
-- @param old_file_path string Path to the original file
-- @param new_file_path string Path to the new file (used for naming)
-- @param new_file_contents string Contents of the new file
-- @param tab_name string Name for the diff tab/view
-- @return table Result with provider, tab_name, and success status
function M.open_diff(old_file_path, new_file_path, new_file_contents, tab_name)
  return M._open_native_diff(old_file_path, new_file_path, new_file_contents, tab_name)
end

--- Create a temporary file with content
-- @param content string The content to write
-- @param filename string Base filename for the temporary file
-- @return string|nil, string|nil The temporary file path and error message
local function create_temp_file(content, filename)
  local base_dir_cache = vim.fn.stdpath("cache") .. "/claudecode_diffs"
  local mkdir_ok_cache, mkdir_err_cache = pcall(vim.fn.mkdir, base_dir_cache, "p")

  local final_base_dir
  if mkdir_ok_cache then
    final_base_dir = base_dir_cache
  else
    local base_dir_temp = vim.fn.stdpath("cache") .. "/claudecode_diffs_fallback"
    local mkdir_ok_temp, mkdir_err_temp = pcall(vim.fn.mkdir, base_dir_temp, "p")
    if not mkdir_ok_temp then
      local err_to_report = mkdir_err_temp or mkdir_err_cache or "unknown error creating base temp dir"
      return nil, "Failed to create base temporary directory: " .. tostring(err_to_report)
    end
    final_base_dir = base_dir_temp
  end

  local session_id_base = vim.fn.fnamemodify(vim.fn.tempname(), ":t")
    .. "_"
    .. tostring(os.time())
    .. "_"
    .. tostring(math.random(1000, 9999))
  local session_id = session_id_base:gsub("[^A-Za-z0-9_-]", "")
  if session_id == "" then -- Fallback if all characters were problematic, ensuring a directory can be made.
    session_id = "claudecode_session"
  end

  local tmp_session_dir = final_base_dir .. "/" .. session_id
  local mkdir_session_ok, mkdir_session_err = pcall(vim.fn.mkdir, tmp_session_dir, "p")
  if not mkdir_session_ok then
    return nil, "Failed to create temporary session directory: " .. tostring(mkdir_session_err)
  end

  local tmp_file = tmp_session_dir .. "/" .. filename
  local file = io.open(tmp_file, "w")
  if not file then
    return nil, "Failed to create temporary file: " .. tmp_file
  end

  file:write(content)
  file:close()

  return tmp_file, nil
end

--- Clean up temporary files and directories
-- @param tmp_file string Path to the temporary file to clean up
local function cleanup_temp_file(tmp_file)
  if tmp_file and vim.fn.filereadable(tmp_file) == 1 then
    local tmp_dir = vim.fn.fnamemodify(tmp_file, ":h")
    if vim.fs and type(vim.fs.remove) == "function" then
      local ok_file, err_file = pcall(vim.fs.remove, tmp_file)
      if not ok_file then
        vim.notify(
          "ClaudeCode: Error removing temp file " .. tmp_file .. ": " .. tostring(err_file),
          vim.log.levels.WARN
        )
      end

      local ok_dir, err_dir = pcall(vim.fs.remove, tmp_dir)
      if not ok_dir then
        vim.notify(
          "ClaudeCode: Error removing temp directory " .. tmp_dir .. ": " .. tostring(err_dir),
          vim.log.levels.INFO
        )
      end
    else
      local reason = "vim.fs.remove is not a function"
      if not vim.fs then
        reason = "vim.fs is nil"
      end
      vim.notify(
        "ClaudeCode: Cannot perform standard cleanup: "
          .. reason
          .. ". Affected file: "
          .. tmp_file
          .. ". Please check your Neovim setup or report this issue.",
        vim.log.levels.ERROR
      )
      -- Fallback to os.remove for the file.
      local os_ok, os_err = pcall(os.remove, tmp_file)
      if not os_ok then
        vim.notify(
          "ClaudeCode: Fallback os.remove also failed for file " .. tmp_file .. ": " .. tostring(os_err),
          vim.log.levels.ERROR
        )
      end
    end
  end
end

-- Detect filetype from a path or existing buffer (best-effort)
local function detect_filetype(path, buf)
  -- 1) Try Neovim's builtin matcher if available (>=0.10)
  if vim.filetype and type(vim.filetype.match) == "function" then
    local ok, ft = pcall(vim.filetype.match, { filename = path })
    if ok and ft and ft ~= "" then
      return ft
    end
  end

  -- 2) Try reading from existing buffer
  if buf and vim.api.nvim_buf_is_valid(buf) then
    local ft = vim.api.nvim_buf_get_option(buf, "filetype")
    if ft and ft ~= "" then
      return ft
    end
  end

  -- 3) Fallback to simple extension mapping
  local ext = path:match("%.([%w_%-]+)$") or ""
  local simple_map = {
    lua = "lua",
    ts = "typescript",
    js = "javascript",
    jsx = "javascriptreact",
    tsx = "typescriptreact",
    py = "python",
    go = "go",
    rs = "rust",
    c = "c",
    h = "c",
    cpp = "cpp",
    hpp = "cpp",
    md = "markdown",
    sh = "sh",
    zsh = "zsh",
    bash = "bash",
    json = "json",
    yaml = "yaml",
    yml = "yaml",
    toml = "toml",
  }
  return simple_map[ext]
end
--- Open diff using native Neovim functionality
-- @param old_file_path string Path to the original file
-- @param new_file_path string Path to the new file (used for naming)
-- @param new_file_contents string Contents of the new file
-- @param tab_name string Name for the diff tab/view
-- @return table Result with provider, tab_name, and success status
function M._open_native_diff(old_file_path, new_file_path, new_file_contents, tab_name)
  local new_filename = vim.fn.fnamemodify(new_file_path, ":t") .. ".new"
  local tmp_file, err = create_temp_file(new_file_contents, new_filename)
  if not tmp_file then
    return { provider = "native", tab_name = tab_name, success = false, error = err, temp_file = nil }
  end

  local target_win = find_main_editor_window()

  if target_win then
    vim.api.nvim_set_current_win(target_win)
  else
    vim.cmd("wincmd t")
    vim.cmd("wincmd l")
    local buf = vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win())
    local buftype = vim.api.nvim_buf_get_option(buf, "buftype")

    if buftype == "terminal" or buftype == "nofile" then
      vim.cmd("vsplit")
    end
  end

  vim.cmd("edit " .. vim.fn.fnameescape(old_file_path))
  vim.cmd("diffthis")
  vim.cmd("vsplit")
  vim.cmd("edit " .. vim.fn.fnameescape(tmp_file))
  vim.api.nvim_buf_set_name(0, new_file_path .. " (New)")

  -- Propagate filetype to the proposed buffer for proper syntax highlighting (#20)
  local proposed_buf = vim.api.nvim_get_current_buf()
  local old_filetype = detect_filetype(old_file_path)
  if old_filetype and old_filetype ~= "" then
    vim.api.nvim_set_option_value("filetype", old_filetype, { buf = proposed_buf })
  end

  vim.cmd("wincmd =")

  local new_buf = proposed_buf
  vim.api.nvim_set_option_value("buftype", "nofile", { buf = new_buf })
  vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = new_buf })
  vim.api.nvim_set_option_value("swapfile", false, { buf = new_buf })

  vim.cmd("diffthis")

  local cleanup_group = vim.api.nvim_create_augroup("ClaudeCodeDiffCleanup", { clear = false })
  vim.api.nvim_create_autocmd({ "BufDelete", "BufWipeout" }, {
    group = cleanup_group,
    buffer = new_buf,
    callback = function()
      cleanup_temp_file(tmp_file)
    end,
    once = true,
  })

  return {
    provider = "native",
    tab_name = tab_name,
    success = true,
    temp_file = tmp_file,
  }
end

--- Register diff state for tracking
-- @param tab_name string Unique identifier for the diff
-- @param diff_data table Diff state data
function M._register_diff_state(tab_name, diff_data)
  active_diffs[tab_name] = diff_data
end

--- Resolve diff as saved (user accepted changes)
-- @param tab_name string The diff identifier
-- @param buffer_id number The buffer that was saved
function M._resolve_diff_as_saved(tab_name, buffer_id)
  local diff_data = active_diffs[tab_name]
  if not diff_data or diff_data.status ~= "pending" then
    return
  end

  logger.debug("diff", "Resolving diff as saved for", tab_name, "from buffer", buffer_id)

  -- Get content from buffer
  local content_lines = vim.api.nvim_buf_get_lines(buffer_id, 0, -1, false)
  local final_content = table.concat(content_lines, "\n")
  -- Add trailing newline if the buffer has one
  if #content_lines > 0 and vim.api.nvim_buf_get_option(buffer_id, "eol") then
    final_content = final_content .. "\n"
  end

  -- Close diff windows (unified behavior)
  if diff_data.new_window and vim.api.nvim_win_is_valid(diff_data.new_window) then
    vim.api.nvim_win_close(diff_data.new_window, true)
  end
  if diff_data.target_window and vim.api.nvim_win_is_valid(diff_data.target_window) then
    vim.api.nvim_set_current_win(diff_data.target_window)
    vim.cmd("diffoff")
  end

  -- Create MCP-compliant response
  local result = {
    content = {
      { type = "text", text = "FILE_SAVED" },
      { type = "text", text = final_content },
    },
  }

  diff_data.status = "saved"
  diff_data.result_content = result

  -- Resume the coroutine with the result (for deferred response system)
  if diff_data.resolution_callback then
    logger.debug("diff", "Resuming coroutine for saved diff", tab_name)
    diff_data.resolution_callback(result)
  else
    logger.debug("diff", "No resolution callback found for saved diff", tab_name)
  end

  -- Reload the original file buffer after a delay to ensure Claude CLI has written the file
  vim.defer_fn(function()
    local current_diff_data = active_diffs[tab_name]
    local original_cursor_pos = current_diff_data and current_diff_data.original_cursor_pos
    M.reload_file_buffers_manual(diff_data.old_file_path, original_cursor_pos)
  end, 200)

  -- NOTE: Diff state cleanup is handled by close_tab tool or explicit cleanup calls
  logger.debug("diff", "Diff saved, awaiting close_tab command for cleanup")
end

--- Reload file buffers after external changes (called when diff is closed)
-- @param file_path string Path to the file that was externally modified
-- @param original_cursor_pos table|nil Original cursor position to restore {row, col}
local function reload_file_buffers(file_path, original_cursor_pos)
  logger.debug("diff", "Reloading buffers for file:", file_path, original_cursor_pos and "(restoring cursor)" or "")

  local reloaded_count = 0
  -- Find and reload any open buffers for this file
  for _, buf in ipairs(vim.api.nvim_list_bufs()) do
    if vim.api.nvim_buf_is_valid(buf) then
      local buf_name = vim.api.nvim_buf_get_name(buf)

      -- Simple string match - if buffer name matches the file path
      if buf_name == file_path then
        -- Check if buffer is modified - only reload unmodified buffers for safety
        local modified = vim.api.nvim_buf_get_option(buf, "modified")
        logger.debug("diff", "Found matching buffer", buf, "modified:", modified)

        if not modified then
          -- Try to find a window displaying this buffer for proper context
          local win_id = nil
          for _, win in ipairs(vim.api.nvim_list_wins()) do
            if vim.api.nvim_win_get_buf(win) == buf then
              win_id = win
              break
            end
          end

          if win_id then
            vim.api.nvim_win_call(win_id, function()
              vim.cmd("edit")
              -- Restore original cursor position if we have it
              if original_cursor_pos then
                pcall(vim.api.nvim_win_set_cursor, win_id, original_cursor_pos)
              end
            end)
          else
            vim.api.nvim_buf_call(buf, function()
              vim.cmd("edit")
            end)
          end

          reloaded_count = reloaded_count + 1
        end
      end
    end
  end

  logger.debug("diff", "Completed buffer reload - reloaded", reloaded_count, "buffers for file:", file_path)
end

--- Resolve diff as rejected (user closed/rejected)
-- @param tab_name string The diff identifier
function M._resolve_diff_as_rejected(tab_name)
  local diff_data = active_diffs[tab_name]
  if not diff_data or diff_data.status ~= "pending" then
    return
  end

  -- Create MCP-compliant response
  local result = {
    content = {
      { type = "text", text = "DIFF_REJECTED" },
      { type = "text", text = tab_name },
    },
  }

  diff_data.status = "rejected"
  diff_data.result_content = result

  -- Clean up diff state and resources BEFORE resolving to prevent any interference
  M._cleanup_diff_state(tab_name, "diff rejected")

  -- Use vim.schedule to ensure the resolution callback happens after all cleanup
  vim.schedule(function()
    -- Resume the coroutine with the result (for deferred response system)
    if diff_data.resolution_callback then
      logger.debug("diff", "Resuming coroutine for rejected diff", tab_name)
      diff_data.resolution_callback(result)
    end
  end)
end

--- Register autocmds for a specific diff
-- @param tab_name string The diff identifier
-- @param new_buffer number New file buffer ID
-- @return table List of autocmd IDs
local function register_diff_autocmds(tab_name, new_buffer)
  local autocmd_ids = {}

  -- Handle :w command to accept diff changes (replaces both BufWritePost and BufWriteCmd)
  autocmd_ids[#autocmd_ids + 1] = vim.api.nvim_create_autocmd("BufWriteCmd", {
    group = get_autocmd_group(),
    buffer = new_buffer,
    callback = function()
      logger.debug("diff", "BufWriteCmd (:w) triggered - accepting diff changes for", tab_name)
      M._resolve_diff_as_saved(tab_name, new_buffer)
      -- Prevent actual file write since we're handling it through MCP
      return true
    end,
  })

  -- Buffer deletion monitoring for rejection (multiple events to catch all deletion methods)

  -- BufDelete: When buffer is deleted with :bdelete, :bwipeout, etc.
  autocmd_ids[#autocmd_ids + 1] = vim.api.nvim_create_autocmd("BufDelete", {
    group = get_autocmd_group(),
    buffer = new_buffer,
    callback = function()
      logger.debug("diff", "BufDelete triggered for new buffer", new_buffer, "tab:", tab_name)
      M._resolve_diff_as_rejected(tab_name)
    end,
  })

  -- BufUnload: When buffer is unloaded (covers more scenarios)
  autocmd_ids[#autocmd_ids + 1] = vim.api.nvim_create_autocmd("BufUnload", {
    group = get_autocmd_group(),
    buffer = new_buffer,
    callback = function()
      logger.debug("diff", "BufUnload triggered for new buffer", new_buffer, "tab:", tab_name)
      M._resolve_diff_as_rejected(tab_name)
    end,
  })

  -- BufWipeout: When buffer is wiped out completely
  autocmd_ids[#autocmd_ids + 1] = vim.api.nvim_create_autocmd("BufWipeout", {
    group = get_autocmd_group(),
    buffer = new_buffer,
    callback = function()
      logger.debug("diff", "BufWipeout triggered for new buffer", new_buffer, "tab:", tab_name)
      M._resolve_diff_as_rejected(tab_name)
    end,
  })

  -- Note: We intentionally do NOT monitor old_buffer for deletion
  -- because it's the actual file buffer and shouldn't trigger diff rejection

  return autocmd_ids
end

--- Create diff view from a specific window
-- @param target_window number The window to use as base for the diff
-- @param old_file_path string Path to the original file
-- @param new_buffer number New file buffer ID
-- @param tab_name string The diff identifier
-- @param is_new_file boolean Whether this is a new file (doesn't exist yet)
-- @return table Info about the created diff layout
function M._create_diff_view_from_window(target_window, old_file_path, new_buffer, tab_name, is_new_file)
  -- If no target window provided, create a new window in suitable location
  if not target_window then
    -- Try to create a new window in the main area
    vim.cmd("wincmd t") -- Go to top-left
    vim.cmd("wincmd l") -- Move right (to middle if layout is left|middle|right)

    local buf = vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win())
    local buftype = vim.api.nvim_buf_get_option(buf, "buftype")
    local filetype = vim.api.nvim_buf_get_option(buf, "filetype")

    if buftype == "terminal" or buftype == "prompt" or filetype == "neo-tree" or filetype == "ClaudeCode" then
      vim.cmd("vsplit")
    end

    target_window = vim.api.nvim_get_current_win()
  else
    vim.api.nvim_set_current_win(target_window)
  end

  local original_buffer
  if is_new_file then
    local empty_buffer = vim.api.nvim_create_buf(false, true)
    if not empty_buffer or empty_buffer == 0 then
      local error_msg = "Failed to create empty buffer for new file diff"
      logger.error("diff", error_msg)
      error({
        code = -32000,
        message = "Buffer creation failed",
        data = error_msg,
      })
    end

    -- Set buffer properties with error handling
    local success, err = pcall(function()
      vim.api.nvim_buf_set_name(empty_buffer, old_file_path .. " (NEW FILE)")
      vim.api.nvim_buf_set_lines(empty_buffer, 0, -1, false, {})
      vim.api.nvim_buf_set_option(empty_buffer, "buftype", "nofile")
      vim.api.nvim_buf_set_option(empty_buffer, "modifiable", false)
      vim.api.nvim_buf_set_option(empty_buffer, "readonly", true)
    end)

    if not success then
      pcall(vim.api.nvim_buf_delete, empty_buffer, { force = true })
      local error_msg = "Failed to configure empty buffer: " .. tostring(err)
      logger.error("diff", error_msg)
      error({
        code = -32000,
        message = "Buffer configuration failed",
        data = error_msg,
      })
    end

    vim.api.nvim_win_set_buf(target_window, empty_buffer)
    original_buffer = empty_buffer
  else
    vim.cmd("edit " .. vim.fn.fnameescape(old_file_path))
    original_buffer = vim.api.nvim_win_get_buf(target_window)
  end

  vim.cmd("diffthis")

  vim.cmd("vsplit")
  local new_win = vim.api.nvim_get_current_win()
  vim.api.nvim_win_set_buf(new_win, new_buffer)

  -- Ensure new buffer inherits filetype from original for syntax highlighting (#20)
  local original_ft = detect_filetype(old_file_path, original_buffer)
  if original_ft and original_ft ~= "" then
    vim.api.nvim_set_option_value("filetype", original_ft, { buf = new_buffer })
  end
  vim.cmd("diffthis")

  vim.cmd("wincmd =")
  vim.api.nvim_set_current_win(new_win)

  -- Store diff context in buffer variables for user commands
  vim.b[new_buffer].claudecode_diff_tab_name = tab_name
  vim.b[new_buffer].claudecode_diff_new_win = new_win
  vim.b[new_buffer].claudecode_diff_target_win = target_window

  -- Return window information for later storage
  return {
    new_window = new_win,
    target_window = target_window,
    original_buffer = original_buffer,
  }
end

--- Clean up diff state and resources
-- @param tab_name string The diff identifier
-- @param reason string Reason for cleanup
function M._cleanup_diff_state(tab_name, reason)
  local diff_data = active_diffs[tab_name]
  if not diff_data then
    return
  end

  -- Clean up autocmds
  for _, autocmd_id in ipairs(diff_data.autocmd_ids or {}) do
    pcall(vim.api.nvim_del_autocmd, autocmd_id)
  end

  -- Clean up the new buffer only (not the old buffer which is the user's file)
  if diff_data.new_buffer and vim.api.nvim_buf_is_valid(diff_data.new_buffer) then
    pcall(vim.api.nvim_buf_delete, diff_data.new_buffer, { force = true })
  end

  -- Close new diff window if still open
  if diff_data.new_window and vim.api.nvim_win_is_valid(diff_data.new_window) then
    pcall(vim.api.nvim_win_close, diff_data.new_window, true)
  end

  -- Turn off diff mode in target window if it still exists
  if diff_data.target_window and vim.api.nvim_win_is_valid(diff_data.target_window) then
    vim.api.nvim_win_call(diff_data.target_window, function()
      vim.cmd("diffoff")
    end)
  end

  -- Remove from active diffs
  active_diffs[tab_name] = nil

  logger.debug("diff", "Cleaned up diff state for", tab_name, "due to:", reason)
end

--- Clean up all active diffs
-- @param reason string Reason for cleanup
-- NOTE: This will become a public closeAllDiffTabs tool in the future
function M._cleanup_all_active_diffs(reason)
  for tab_name, _ in pairs(active_diffs) do
    M._cleanup_diff_state(tab_name, reason)
  end
end

--- Set up blocking diff operation with simpler approach
-- @param params table Parameters for the diff
-- @param resolution_callback function Callback to call when diff resolves
function M._setup_blocking_diff(params, resolution_callback)
  local tab_name = params.tab_name
  logger.debug("diff", "Setting up diff for:", params.old_file_path)

  -- Wrap the setup in error handling to ensure cleanup on failure
  local setup_success, setup_error = pcall(function()
    -- Step 1: Check if the file exists (allow new files)
    local old_file_exists = vim.fn.filereadable(params.old_file_path) == 1
    local is_new_file = not old_file_exists

    -- Step 2: Find if the file is already open in a buffer (only for existing files)
    local existing_buffer = nil
    local target_window = nil

    if old_file_exists then
      -- Look for existing buffer with this file
      for _, buf in ipairs(vim.api.nvim_list_bufs()) do
        if vim.api.nvim_buf_is_valid(buf) and vim.api.nvim_buf_is_loaded(buf) then
          local buf_name = vim.api.nvim_buf_get_name(buf)
          if buf_name == params.old_file_path then
            existing_buffer = buf
            break
          end
        end
      end

      -- Find window containing this buffer (if any)
      if existing_buffer then
        for _, win in ipairs(vim.api.nvim_list_wins()) do
          if vim.api.nvim_win_get_buf(win) == existing_buffer then
            target_window = win
            break
          end
        end
      end
    end

    -- If no existing buffer/window, find a suitable main editor window
    if not target_window then
      target_window = find_main_editor_window()
    end

    -- Step 3: Create scratch buffer for new content
    local new_buffer = vim.api.nvim_create_buf(false, true) -- unlisted, scratch
    if new_buffer == 0 then
      error({
        code = -32000,
        message = "Buffer creation failed",
        data = "Could not create new content buffer",
      })
    end

    local new_unique_name = is_new_file and (tab_name .. " (NEW FILE - proposed)") or (tab_name .. " (proposed)")
    vim.api.nvim_buf_set_name(new_buffer, new_unique_name)
    local lines = vim.split(params.new_file_contents, "\n")
    -- Remove trailing empty line if content ended with \n
    if #lines > 0 and lines[#lines] == "" then
      table.remove(lines, #lines)
    end
    vim.api.nvim_buf_set_lines(new_buffer, 0, -1, false, lines)

    vim.api.nvim_buf_set_option(new_buffer, "buftype", "acwrite") -- Allows saving but stays as scratch-like
    vim.api.nvim_buf_set_option(new_buffer, "modifiable", true)

    -- Step 4: Set up diff view using the target window
    local diff_info =
      M._create_diff_view_from_window(target_window, params.old_file_path, new_buffer, tab_name, is_new_file)

    -- Step 5: Register autocmds for user interaction monitoring
    local autocmd_ids = register_diff_autocmds(tab_name, new_buffer)

    -- Step 6: Store diff state

    -- Save the original cursor position before storing diff state
    local original_cursor_pos = nil
    if diff_info.target_window and vim.api.nvim_win_is_valid(diff_info.target_window) then
      original_cursor_pos = vim.api.nvim_win_get_cursor(diff_info.target_window)
    end

    M._register_diff_state(tab_name, {
      old_file_path = params.old_file_path,
      new_file_path = params.new_file_path,
      new_file_contents = params.new_file_contents,
      new_buffer = new_buffer,
      new_window = diff_info.new_window,
      target_window = diff_info.target_window,
      original_buffer = diff_info.original_buffer,
      original_cursor_pos = original_cursor_pos,
      autocmd_ids = autocmd_ids,
      created_at = vim.fn.localtime(),
      status = "pending",
      resolution_callback = resolution_callback,
      result_content = nil,
      is_new_file = is_new_file,
    })
  end) -- End of pcall

  -- Handle setup errors
  if not setup_success then
    local error_msg = "Failed to setup diff operation: " .. tostring(setup_error)
    logger.error("diff", error_msg)

    -- Clean up any partial state that might have been created
    if active_diffs[tab_name] then
      M._cleanup_diff_state(tab_name, "setup failed")
    end

    -- Re-throw the error for MCP compliance
    error({
      code = -32000,
      message = "Diff setup failed",
      data = error_msg,
    })
  end
end

--- Blocking diff operation for MCP compliance
-- @param old_file_path string Path to the original file
-- @param new_file_path string Path to the new file (used for naming)
-- @param new_file_contents string Contents of the new file
-- @param tab_name string Name for the diff tab/view
-- @return table MCP-compliant response with content array
function M.open_diff_blocking(old_file_path, new_file_path, new_file_contents, tab_name)
  -- Check for existing diff with same tab_name
  if active_diffs[tab_name] then
    -- Resolve the existing diff as rejected before replacing
    M._resolve_diff_as_rejected(tab_name)
  end

  -- Set up blocking diff operation
  local co = coroutine.running()
  if not co then
    error({
      code = -32000,
      message = "Internal server error",
      data = "openDiff must run in coroutine context",
    })
  end

  logger.debug("diff", "Starting diff setup for", tab_name)

  -- Use native diff implementation
  local success, err = pcall(M._setup_blocking_diff, {
    old_file_path = old_file_path,
    new_file_path = new_file_path,
    new_file_contents = new_file_contents,
    tab_name = tab_name,
  }, function(result)
    -- Resume the coroutine with the result
    local resume_success, resume_result = coroutine.resume(co, result)
    if resume_success then
      -- Use the global response sender to avoid module reloading issues
      local co_key = tostring(co)
      if _G.claude_deferred_responses and _G.claude_deferred_responses[co_key] then
        _G.claude_deferred_responses[co_key](resume_result)
        _G.claude_deferred_responses[co_key] = nil
      else
        logger.error("diff", "No global response sender found for coroutine:", co_key)
      end
    else
      logger.error("diff", "Coroutine failed:", tostring(resume_result))
      local co_key = tostring(co)
      if _G.claude_deferred_responses and _G.claude_deferred_responses[co_key] then
        _G.claude_deferred_responses[co_key]({
          error = {
            code = -32603,
            message = "Internal error",
            data = "Coroutine failed: " .. tostring(resume_result),
          },
        })
        _G.claude_deferred_responses[co_key] = nil
      end
    end
  end)

  if not success then
    logger.error("diff", "Diff setup failed for", tab_name, "error:", tostring(err))
    -- If the error is already structured, propagate it directly
    if type(err) == "table" and err.code then
      error(err)
    else
      error({
        code = -32000,
        message = "Error setting up diff",
        data = tostring(err),
      })
    end
  end

  -- Yield and wait indefinitely for user interaction - the resolve functions will resume us
  local user_action_result = coroutine.yield()
  logger.debug("diff", "User action completed for", tab_name)

  -- Return the result directly - this will be sent by the deferred response system
  return user_action_result
end

-- Set up global autocmds for shutdown handling
vim.api.nvim_create_autocmd("VimLeavePre", {
  group = get_autocmd_group(),
  callback = function()
    M._cleanup_all_active_diffs("shutdown")
  end,
})

--- Close diff by tab name (used by close_tab tool)
-- @param tab_name string The diff identifier
-- @return boolean success True if diff was found and closed
function M.close_diff_by_tab_name(tab_name)
  local diff_data = active_diffs[tab_name]
  if not diff_data then
    return false
  end

  -- If the diff was already saved, reload file buffers and clean up
  if diff_data.status == "saved" then
    -- Claude Code CLI has written the file, reload any open buffers
    if diff_data.old_file_path then
      -- Add a small delay to ensure Claude CLI has finished writing the file
      vim.defer_fn(function()
        M.reload_file_buffers_manual(diff_data.old_file_path, diff_data.original_cursor_pos)
      end, 100) -- 100ms delay
    end
    M._cleanup_diff_state(tab_name, "diff tab closed after save")
    return true
  end

  -- If still pending, treat as rejection
  if diff_data.status == "pending" then
    M._resolve_diff_as_rejected(tab_name)
    return true
  end

  return false
end

-- Test helper function (only for testing)
function M._get_active_diffs()
  return active_diffs
end

-- Manual buffer reload function for testing/debugging
function M.reload_file_buffers_manual(file_path, original_cursor_pos)
  return reload_file_buffers(file_path, original_cursor_pos)
end

--- Accept the current diff (user command version)
-- This function reads the diff context from buffer variables
function M.accept_current_diff()
  local current_buffer = vim.api.nvim_get_current_buf()
  local tab_name = vim.b[current_buffer].claudecode_diff_tab_name

  if not tab_name then
    vim.notify("No active diff found in current buffer", vim.log.levels.WARN)
    return
  end

  M._resolve_diff_as_saved(tab_name, current_buffer)
end

--- Deny/reject the current diff (user command version)
-- This function reads the diff context from buffer variables
function M.deny_current_diff()
  local current_buffer = vim.api.nvim_get_current_buf()
  local tab_name = vim.b[current_buffer].claudecode_diff_tab_name
  local new_win = vim.b[current_buffer].claudecode_diff_new_win
  local target_window = vim.b[current_buffer].claudecode_diff_target_win

  if not tab_name then
    vim.notify("No active diff found in current buffer", vim.log.levels.WARN)
    return
  end

  -- Close windows and clean up (same logic as the original keymap)
  if new_win and vim.api.nvim_win_is_valid(new_win) then
    vim.api.nvim_win_close(new_win, true)
  end
  if target_window and vim.api.nvim_win_is_valid(target_window) then
    vim.api.nvim_set_current_win(target_window)
    vim.cmd("diffoff")
  end

  M._resolve_diff_as_rejected(tab_name)
end

return M

---
-- Manages selection tracking and communication with the Claude server.
-- @module claudecode.selection
local M = {}

local logger = require("claudecode.logger")
local terminal = require("claudecode.terminal")

M.state = {
  latest_selection = nil,
  tracking_enabled = false,
  debounce_timer = nil,
  debounce_ms = 100,

  last_active_visual_selection = nil,
  demotion_timer = nil,
  visual_demotion_delay_ms = 50,
}

--- Enables selection tracking.
-- @param server table The server object to use for communication.
-- @param visual_demotion_delay_ms number The delay for visual selection demotion.
function M.enable(server, visual_demotion_delay_ms)
  if M.state.tracking_enabled then
    return
  end

  M.state.tracking_enabled = true
  M.server = server
  M.state.visual_demotion_delay_ms = visual_demotion_delay_ms

  M._create_autocommands()
end

--- Disables selection tracking.
-- Clears autocommands, resets internal state, and stops any active debounce timers.
function M.disable()
  if not M.state.tracking_enabled then
    return
  end

  M.state.tracking_enabled = false

  M._clear_autocommands()

  M.state.latest_selection = nil
  M.server = nil

  if M.state.debounce_timer then
    vim.loop.timer_stop(M.state.debounce_timer)
    M.state.debounce_timer = nil
  end
end

--- Creates autocommands for tracking selections.
-- Sets up listeners for CursorMoved, CursorMovedI, ModeChanged, and TextChanged events.
-- @local
function M._create_autocommands()
  local group = vim.api.nvim_create_augroup("ClaudeCodeSelection", { clear = true })

  vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, {
    group = group,
    callback = function()
      M.on_cursor_moved()
    end,
  })

  vim.api.nvim_create_autocmd("ModeChanged", {
    group = group,
    callback = function()
      M.on_mode_changed()
    end,
  })

  vim.api.nvim_create_autocmd("TextChanged", {
    group = group,
    callback = function()
      M.on_text_changed()
    end,
  })
end

--- Clears the autocommands related to selection tracking.
-- @local
function M._clear_autocommands()
  vim.api.nvim_clear_autocmds({ group = "ClaudeCodeSelection" })
end

--- Handles cursor movement events.
-- Triggers a debounced update of the selection.
function M.on_cursor_moved()
  M.debounce_update()
end

--- Handles mode change events.
-- Triggers an immediate update of the selection.
function M.on_mode_changed()
  M.update_selection()
end

--- Handles text change events.
-- Triggers a debounced update of the selection.
function M.on_text_changed()
  M.debounce_update()
end

--- Debounces selection updates.
-- Ensures that `update_selection` is not called too frequently by deferring
-- its execution.
function M.debounce_update()
  if M.state.debounce_timer then
    vim.loop.timer_stop(M.state.debounce_timer)
  end

  M.state.debounce_timer = vim.defer_fn(function()
    M.update_selection()
    M.state.debounce_timer = nil
  end, M.state.debounce_ms)
end

--- Updates the current selection state.
-- Determines the current selection based on the editor mode (visual or normal)
-- and sends an update to the server if the selection has changed.
function M.update_selection()
  if not M.state.tracking_enabled then
    return
  end

  local current_buf = vim.api.nvim_get_current_buf()
  local buf_name = vim.api.nvim_buf_get_name(current_buf)

  -- If the buffer name starts with "✻ [Claude Code]", do not update selection
  if buf_name and string.sub(buf_name, 1, string.len("✻ [Claude Code]")) == "✻ [Claude Code]" then
    -- Optionally, cancel demotion timer like for the terminal
    if M.state.demotion_timer then
      M.state.demotion_timer:stop()
      M.state.demotion_timer:close()
      M.state.demotion_timer = nil
    end
    return
  end

  -- If the current buffer is the Claude terminal, do not update selection
  if terminal then
    local claude_term_bufnr = terminal.get_active_terminal_bufnr()
    if claude_term_bufnr and current_buf == claude_term_bufnr then
      -- Cancel any pending demotion if we switch to the Claude terminal
      if M.state.demotion_timer then
        M.state.demotion_timer:stop()
        M.state.demotion_timer:close()
        M.state.demotion_timer = nil
      end
      return
    end
  end

  local current_mode_info = vim.api.nvim_get_mode()
  local current_mode = current_mode_info.mode
  local current_selection

  if current_mode == "v" or current_mode == "V" or current_mode == "\022" then
    -- If a new visual selection is made, cancel any pending demotion
    if M.state.demotion_timer then
      M.state.demotion_timer:stop()
      M.state.demotion_timer:close()
      M.state.demotion_timer = nil
    end

    current_selection = M.get_visual_selection()

    if current_selection then
      M.state.last_active_visual_selection = {
        bufnr = current_buf,
        selection_data = vim.deepcopy(current_selection), -- Store a copy
        timestamp = vim.loop.now(),
      }
    else
      -- No valid visual selection (e.g., get_visual_selection returned nil)
      -- Clear last_active_visual if it was for this buffer
      if M.state.last_active_visual_selection and M.state.last_active_visual_selection.bufnr == current_buf then
        M.state.last_active_visual_selection = nil
      end
    end
  else
    local last_visual = M.state.last_active_visual_selection

    if M.state.demotion_timer then
      -- A demotion is already pending. For this specific update_selection call (e.g. cursor moved),
      -- current_selection reflects the immediate cursor position.
      -- M.state.latest_selection (the one that might be sent) is still the visual one until timer resolves.
      current_selection = M.get_cursor_position()
    elseif
      last_visual
      and last_visual.bufnr == current_buf
      and last_visual.selection_data
      and not last_visual.selection_data.selection.isEmpty
    then
      -- We just exited visual mode in this buffer, and no demotion timer is running for it.
      -- Keep M.state.latest_selection as is (it's the visual one from the previous update).
      -- The 'current_selection' for comparison should also be this visual one.
      current_selection = M.state.latest_selection

      if M.state.demotion_timer then -- Should not happen due to elseif, but as safeguard
        M.state.demotion_timer:stop()
        M.state.demotion_timer:close()
      end

      M.state.demotion_timer = vim.loop.new_timer()
      M.state.demotion_timer:start(
        M.state.visual_demotion_delay_ms,
        0, -- 0 repeat = one-shot
        vim.schedule_wrap(function()
          if M.state.demotion_timer then -- Check if it wasn't cancelled right before firing
            M.state.demotion_timer:stop()
            M.state.demotion_timer:close()
            M.state.demotion_timer = nil
          end
          M.handle_selection_demotion(current_buf) -- Pass buffer at time of scheduling
        end)
      )
    else
      -- Genuinely in normal mode, no recent visual exit, no pending demotion.
      current_selection = M.get_cursor_position()
      if last_visual and last_visual.bufnr == current_buf then
        M.state.last_active_visual_selection = nil -- Clear it as it's no longer relevant for demotion
      end
    end
  end

  -- If current_selection could not be determined (e.g. get_visual_selection was nil and no other path set it)
  -- default to cursor position to avoid errors.
  if not current_selection then
    current_selection = M.get_cursor_position()
  end

  local changed = M.has_selection_changed(current_selection)

  if changed then
    M.state.latest_selection = current_selection
    if M.server then
      M.send_selection_update(current_selection)
    end
  end
end

--- Handles the demotion of a visual selection after a delay.
-- Called by the demotion_timer.
-- @param original_bufnr_when_scheduled number The buffer number that was active when demotion was scheduled.
function M.handle_selection_demotion(original_bufnr_when_scheduled)
  -- Timer object is already stopped and cleared by its own callback wrapper or cancellation points.
  -- M.state.demotion_timer should be nil here if it fired normally or was cancelled.

  local current_buf = vim.api.nvim_get_current_buf()
  local claude_term_bufnr = terminal.get_active_terminal_bufnr()

  -- Condition 1: Switched to Claude Terminal
  if claude_term_bufnr and current_buf == claude_term_bufnr then
    -- Visual selection is preserved (M.state.latest_selection is still the visual one).
    -- The "pending" status of last_active_visual_selection is resolved.
    if
      M.state.last_active_visual_selection
      and M.state.last_active_visual_selection.bufnr == original_bufnr_when_scheduled
    then
      M.state.last_active_visual_selection = nil
    end
    return
  end

  local current_mode_info = vim.api.nvim_get_mode()

  -- Condition 2: Back in Visual Mode in the Original Buffer
  if
    current_buf == original_bufnr_when_scheduled
    and (current_mode_info.mode == "v" or current_mode_info.mode == "V" or current_mode_info.mode == "\022")
  then
    -- A new visual selection will take precedence. M.state.latest_selection will be updated by main flow.
    if
      M.state.last_active_visual_selection
      and M.state.last_active_visual_selection.bufnr == original_bufnr_when_scheduled
    then
      M.state.last_active_visual_selection = nil
    end
    return
  end

  -- Condition 3: Still in Original Buffer & Not Visual & Not Claude Term -> Demote
  if current_buf == original_bufnr_when_scheduled then
    local new_sel_for_demotion = M.get_cursor_position()
    -- Check if this new cursor position is actually different from the (visual) latest_selection
    if M.has_selection_changed(new_sel_for_demotion) then
      M.state.latest_selection = new_sel_for_demotion
      if M.server then
        M.send_selection_update(M.state.latest_selection)
      end
    end
    -- No change detected in selection
  end
  -- User switched to different buffer

  -- Always clear last_active_visual_selection for the original buffer as its pending demotion is resolved.
  if
    M.state.last_active_visual_selection
    and M.state.last_active_visual_selection.bufnr == original_bufnr_when_scheduled
  then
    M.state.last_active_visual_selection = nil
  end
end

--- Validates if we're in a valid visual selection mode
-- @return boolean, string|nil - true if valid, false and error message if not
local function validate_visual_mode()
  local current_nvim_mode = vim.api.nvim_get_mode().mode
  local fixed_anchor_pos_raw = vim.fn.getpos("v")

  if not (current_nvim_mode == "v" or current_nvim_mode == "V" or current_nvim_mode == "\22") then
    return false, "not in visual mode"
  end

  if fixed_anchor_pos_raw[2] == 0 then
    return false, "no visual selection mark"
  end

  return true, nil
end

--- Determines the effective visual mode character
-- @return string|nil - the visual mode character or nil if invalid
local function get_effective_visual_mode()
  local current_nvim_mode = vim.api.nvim_get_mode().mode
  local visual_fn_mode_char = vim.fn.visualmode()

  if visual_fn_mode_char and visual_fn_mode_char ~= "" then
    return visual_fn_mode_char
  end

  -- Fallback to current mode
  if current_nvim_mode == "V" then
    return "V"
  elseif current_nvim_mode == "v" then
    return "v"
  elseif current_nvim_mode == "\22" then -- Ctrl-V, blockwise
    return "\22"
  end

  return nil
end

--- Gets the start and end coordinates of the visual selection
-- @return table, table - start_coords and end_coords with lnum and col fields
local function get_selection_coordinates()
  local fixed_anchor_pos_raw = vim.fn.getpos("v")
  local current_cursor_nvim = vim.api.nvim_win_get_cursor(0)

  -- Convert to 1-indexed line and 1-indexed column for consistency
  local p1 = { lnum = fixed_anchor_pos_raw[2], col = fixed_anchor_pos_raw[3] }
  local p2 = { lnum = current_cursor_nvim[1], col = current_cursor_nvim[2] + 1 }

  -- Determine chronological start/end based on line, then column
  if p1.lnum < p2.lnum or (p1.lnum == p2.lnum and p1.col <= p2.col) then
    return p1, p2
  else
    return p2, p1
  end
end

--- Extracts text for linewise visual selection
-- @param lines_content table - array of line strings
-- @param start_coords table - start coordinates
-- @return string - the extracted text
local function extract_linewise_text(lines_content, start_coords)
  start_coords.col = 1 -- Linewise selection effectively starts at column 1
  return table.concat(lines_content, "\n")
end

--- Extracts text for characterwise visual selection
-- @param lines_content table - array of line strings
-- @param start_coords table - start coordinates
-- @param end_coords table - end coordinates
-- @return string|nil - the extracted text or nil if invalid
local function extract_characterwise_text(lines_content, start_coords, end_coords)
  if start_coords.lnum == end_coords.lnum then
    if not lines_content[1] then
      return nil
    end
    return string.sub(lines_content[1], start_coords.col, end_coords.col)
  else
    if not lines_content[1] or not lines_content[#lines_content] then
      return nil
    end

    local text_parts = {}
    table.insert(text_parts, string.sub(lines_content[1], start_coords.col))
    for i = 2, #lines_content - 1 do
      table.insert(text_parts, lines_content[i])
    end
    table.insert(text_parts, string.sub(lines_content[#lines_content], 1, end_coords.col))
    return table.concat(text_parts, "\n")
  end
end

--- Calculates LSP-compatible position coordinates
-- @param start_coords table - start coordinates
-- @param end_coords table - end coordinates
-- @param visual_mode string - the visual mode character
-- @param lines_content table - array of line strings
-- @return table - LSP position object with start and end fields
local function calculate_lsp_positions(start_coords, end_coords, visual_mode, lines_content)
  local lsp_start_line = start_coords.lnum - 1
  local lsp_end_line = end_coords.lnum - 1
  local lsp_start_char, lsp_end_char

  if visual_mode == "V" then
    lsp_start_char = 0 -- Linewise selection always starts at character 0
    -- For linewise, LSP end char is length of the last selected line
    if #lines_content > 0 and lines_content[#lines_content] then
      lsp_end_char = #lines_content[#lines_content]
    else
      lsp_end_char = 0
    end
  else
    lsp_start_char = start_coords.col - 1
    lsp_end_char = end_coords.col
  end

  return {
    start = { line = lsp_start_line, character = lsp_start_char },
    ["end"] = { line = lsp_end_line, character = lsp_end_char },
  }
end

--- Gets the current visual selection details.
-- @return table|nil A table containing selection text, file path, URL, and
--                   start/end positions, or nil if no visual selection exists.
function M.get_visual_selection()
  local valid = validate_visual_mode()
  if not valid then
    return nil
  end

  local visual_mode = get_effective_visual_mode()
  if not visual_mode then
    return nil
  end

  local start_coords, end_coords = get_selection_coordinates()

  local current_buf = vim.api.nvim_get_current_buf()
  local file_path = vim.api.nvim_buf_get_name(current_buf)

  local lines_content = vim.api.nvim_buf_get_lines(
    current_buf,
    start_coords.lnum - 1, -- Convert to 0-indexed
    end_coords.lnum, -- nvim_buf_get_lines end is exclusive
    false
  )

  if #lines_content == 0 then
    return nil
  end

  local final_text
  if visual_mode == "V" then
    final_text = extract_linewise_text(lines_content, start_coords)
  elseif visual_mode == "v" or visual_mode == "\22" then
    final_text = extract_characterwise_text(lines_content, start_coords, end_coords)
    if not final_text then
      return nil
    end
  else
    return nil
  end

  local lsp_positions = calculate_lsp_positions(start_coords, end_coords, visual_mode, lines_content)

  return {
    text = final_text or "",
    filePath = file_path,
    fileUrl = "file://" .. file_path,
    selection = {
      start = lsp_positions.start,
      ["end"] = lsp_positions["end"],
      isEmpty = (not final_text or #final_text == 0),
    },
  }
end

--- Gets the current cursor position when no visual selection is active.
-- @return table A table containing an empty text, file path, URL, and cursor
--               position as start/end, with isEmpty set to true.
function M.get_cursor_position()
  local cursor_pos = vim.api.nvim_win_get_cursor(0)
  local current_buf = vim.api.nvim_get_current_buf()
  local file_path = vim.api.nvim_buf_get_name(current_buf)

  return {
    text = "",
    filePath = file_path,
    fileUrl = "file://" .. file_path,
    selection = {
      start = { line = cursor_pos[1] - 1, character = cursor_pos[2] },
      ["end"] = { line = cursor_pos[1] - 1, character = cursor_pos[2] },
      isEmpty = true,
    },
  }
end

--- Checks if the selection has changed compared to the latest stored selection.
-- @param new_selection table|nil The new selection object to compare.
-- @return boolean true if the selection has changed, false otherwise.
function M.has_selection_changed(new_selection)
  local old_selection = M.state.latest_selection

  if not new_selection then
    return old_selection ~= nil
  end

  if not old_selection then
    return true
  end

  if old_selection.filePath ~= new_selection.filePath then
    return true
  end

  if old_selection.text ~= new_selection.text then
    return true
  end

  if
    old_selection.selection.start.line ~= new_selection.selection.start.line
    or old_selection.selection.start.character ~= new_selection.selection.start.character
    or old_selection.selection["end"].line ~= new_selection.selection["end"].line
    or old_selection.selection["end"].character ~= new_selection.selection["end"].character
  then
    return true
  end

  return false
end

--- Sends the selection update to the Claude server.
-- @param selection table The selection object to send.
function M.send_selection_update(selection)
  M.server.broadcast("selection_changed", selection)
end

--- Gets the latest recorded selection.
-- @return table|nil The latest selection object, or nil if none recorded.
function M.get_latest_selection()
  return M.state.latest_selection
end

--- Sends the current selection to Claude.
-- This function is typically invoked by a user command. It forces an immediate
-- update and sends the latest selection.
function M.send_current_selection()
  if not M.state.tracking_enabled or not M.server then
    logger.error("selection", "Claude Code is not running")
    return
  end

  M.update_selection()

  local selection = M.state.latest_selection

  if not selection then
    logger.error("selection", "No selection available")
    return
  end

  M.send_selection_update(selection)

  vim.api.nvim_echo({ { "Selection sent to Claude", "Normal" } }, false, {})
end

--- Gets selection from range marks (e.g., when using :'<,'> commands)
-- @param line1 number The start line (1-indexed)
-- @param line2 number The end line (1-indexed)
-- @return table|nil A table containing selection text, file path, URL, and
--                   start/end positions, or nil if invalid range
function M.get_range_selection(line1, line2)
  if not line1 or not line2 or line1 < 1 or line2 < 1 or line1 > line2 then
    return nil
  end

  local current_buf = vim.api.nvim_get_current_buf()
  local file_path = vim.api.nvim_buf_get_name(current_buf)

  -- Get the total number of lines in the buffer
  local total_lines = vim.api.nvim_buf_line_count(current_buf)

  -- Ensure line2 doesn't exceed buffer bounds
  if line2 > total_lines then
    line2 = total_lines
  end

  local lines_content = vim.api.nvim_buf_get_lines(
    current_buf,
    line1 - 1, -- Convert to 0-indexed
    line2, -- nvim_buf_get_lines end is exclusive
    false
  )

  if #lines_content == 0 then
    return nil
  end

  local final_text = table.concat(lines_content, "\n")

  -- For range selections, we treat them as linewise
  local lsp_start_line = line1 - 1 -- Convert to 0-indexed
  local lsp_end_line = line2 - 1
  local lsp_start_char = 0
  local lsp_end_char = #lines_content[#lines_content]

  return {
    text = final_text or "",
    filePath = file_path,
    fileUrl = "file://" .. file_path,
    selection = {
      start = { line = lsp_start_line, character = lsp_start_char },
      ["end"] = { line = lsp_end_line, character = lsp_end_char },
      isEmpty = (not final_text or #final_text == 0),
    },
  }
end

--- Sends an at_mentioned notification for the current visual selection.
-- @param line1 number|nil Optional start line for range-based selection
-- @param line2 number|nil Optional end line for range-based selection
function M.send_at_mention_for_visual_selection(line1, line2)
  if not M.state.tracking_enabled then
    logger.error("selection", "Selection tracking is not enabled.")
    return false
  end

  -- Check if Claude Code integration is running (server may or may not have clients)
  local claudecode_main = require("claudecode")
  if not claudecode_main.state.server then
    logger.error("selection", "Claude Code integration is not running.")
    return false
  end

  local sel_to_send

  -- If range parameters are provided, use them (for :'<,'> commands)
  if line1 and line2 then
    sel_to_send = M.get_range_selection(line1, line2)
    if not sel_to_send or sel_to_send.selection.isEmpty then
      logger.warn("selection", "Invalid range selection to send as at-mention.")
      return false
    end
  else
    -- Use existing logic for visual mode or tracked selection
    sel_to_send = M.state.latest_selection

    if not sel_to_send or sel_to_send.selection.isEmpty then
      -- Fallback: try to get current visual selection directly.
      -- This helps if latest_selection was demoted or command was too fast.
      local current_visual = M.get_visual_selection()
      if current_visual and not current_visual.selection.isEmpty then
        sel_to_send = current_visual
      else
        logger.warn("selection", "No visual selection to send as at-mention.")
        return false
      end
    end
  end

  -- Sanity check: ensure the selection is for the current buffer
  local current_buf_name = vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf())
  if sel_to_send.filePath ~= current_buf_name then
    logger.warn(
      "selection",
      "Tracked selection is for '"
        .. sel_to_send.filePath
        .. "', but current buffer is '"
        .. current_buf_name
        .. "'. Not sending."
    )
    return false
  end

  -- Use connection-aware broadcasting from main module
  local file_path = sel_to_send.filePath
  local start_line = sel_to_send.selection.start.line -- Already 0-indexed from selection module
  local end_line = sel_to_send.selection["end"].line -- Already 0-indexed

  local success, error_msg = claudecode_main.send_at_mention(file_path, start_line, end_line, "ClaudeCodeSend")

  if success then
    logger.debug("selection", "Visual selection sent as at-mention.")

    return true
  else
    logger.error("selection", "Failed to send at-mention: " .. (error_msg or "unknown error"))
    return false
  end
end
return M

---
-- 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
          if entry.name ~= ".." and entry.name ~= "." then
            local full_path = current_dir .. entry.name
            -- Handle various entry types
            if entry.type == "file" or entry.type == "link" then
              table.insert(files, full_path)
            elseif entry.type == "directory" then
              -- Ensure directory paths end with /
              table.insert(files, full_path:match("/$") and full_path or full_path .. "/")
            else
              -- For unknown types, return the path anyway
              table.insert(files, full_path)
            end
          end
        end
      end
    end
  end

  return files, nil
end

return M

---
-- Tree integration module for ClaudeCode.nvim
-- Handles detection and selection of files from nvim-tree, neo-tree, and oil.nvim
-- @module claudecode.integrations
local M = {}

--- Get selected files from the current tree explorer
--- @return table|nil files List of file paths, or nil if error
--- @return string|nil error Error message if operation failed
function M.get_selected_files_from_tree()
  local current_ft = vim.bo.filetype

  if current_ft == "NvimTree" then
    return M._get_nvim_tree_selection()
  elseif current_ft == "neo-tree" then
    return M._get_neotree_selection()
  elseif current_ft == "oil" then
    return M._get_oil_selection()
  else
    return nil, "Not in a supported tree buffer (current filetype: " .. current_ft .. ")"
  end
end

--- Get selected files from nvim-tree
--- Supports both multi-selection (marks) and single file under cursor
--- @return table files List of file paths
--- @return string|nil error Error message if operation failed
function M._get_nvim_tree_selection()
  local success, nvim_tree_api = pcall(require, "nvim-tree.api")
  if not success then
    return {}, "nvim-tree not available"
  end

  local files = {}

  local marks = nvim_tree_api.marks.list()

  if marks and #marks > 0 then
    for i, mark in ipairs(marks) do
      if mark.type == "file" and mark.absolute_path and mark.absolute_path ~= "" then
        -- Check if it's not a root-level file (basic protection)
        if not string.match(mark.absolute_path, "^/[^/]*$") then
          table.insert(files, mark.absolute_path)
        end
      end
    end

    if #files > 0 then
      return files, nil
    end
  end

  local node = nvim_tree_api.tree.get_node_under_cursor()
  if node then
    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
        return { node.absolute_path }, nil
      else
        return {}, "Cannot add root-level file. Please select a file in a subdirectory."
      end
    elseif node.type == "directory" and node.absolute_path and node.absolute_path ~= "" then
      return { node.absolute_path }, nil
    end
  end

  return {}, "No file found under cursor"
end

--- Get selected files from neo-tree
--- Uses neo-tree's own visual selection method when in visual mode
--- @return table files List of file paths
--- @return string|nil error Error message if operation failed
function M._get_neotree_selection()
  local success, manager = pcall(require, "neo-tree.sources.manager")
  if not success then
    return {}, "neo-tree not available"
  end

  local state = manager.get_state("filesystem")
  if not state then
    return {}, "neo-tree filesystem state not available"
  end

  local files = {}

  -- Use neo-tree's own visual selection method (like their copy/paste feature)
  local mode = vim.fn.mode()

  if mode == "V" or mode == "v" or mode == "\22" then
    local current_win = vim.api.nvim_get_current_win()

    if state.winid and state.winid == current_win then
      -- Use neo-tree's exact method to get visual range (from their get_selected_nodes implementation)
      local start_pos = vim.fn.getpos("'<")[2]
      local end_pos = vim.fn.getpos("'>")[2]

      -- Fallback to current cursor and anchor if marks are not valid
      if start_pos == 0 or end_pos == 0 then
        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
          start_pos = cursor_pos
          end_pos = cursor_pos
        end
      end

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

      local selected_nodes = {}

      for line = start_pos, end_pos do
        local node = state.tree:get_node(line)
        if node then
          -- Add validation for node types before adding to selection
          if node.type and node.type ~= "message" then
            table.insert(selected_nodes, node)
          end
        end
      end

      for i, node in ipairs(selected_nodes) do
        -- Enhanced validation: check for file type and valid path
        if node.type == "file" and node.path and node.path ~= "" then
          -- Additional check: ensure it's not a root node (depth protection)
          local depth = (node.get_depth and node:get_depth()) and node:get_depth() or 0
          if depth > 1 then
            table.insert(files, node.path)
          end
        end
      end

      if #files > 0 then
        return files, nil
      end
    end
  end

  if state.tree then
    local selection = nil

    if state.tree.get_selection then
      selection = state.tree:get_selection()
    end

    if (not selection or #selection == 0) and state.selected_nodes then
      selection = state.selected_nodes
    end

    if selection and #selection > 0 then
      for i, node in ipairs(selection) do
        if node.type == "file" and node.path then
          table.insert(files, node.path)
        end
      end

      if #files > 0 then
        return files, nil
      end
    end
  end

  if state.tree then
    local node = state.tree:get_node()

    if node then
      if node.type == "file" and node.path then
        return { node.path }, nil
      elseif node.type == "directory" and node.path then
        return { node.path }, nil
      end
    end
  end

  return {}, "No file found under cursor"
end

--- Get selected files from oil.nvim
--- Supports both visual selection and single file under cursor
--- @return table files List of file paths
--- @return string|nil error Error message if operation failed
function M._get_oil_selection()
  local success, oil = pcall(require, "oil")
  if not success then
    return {}, "oil.nvim not available"
  end

  local bufnr = vim.api.nvim_get_current_buf() --[[@as number]]
  local files = {}

  -- Check if we're in visual mode
  local mode = vim.fn.mode()
  if mode == "V" or mode == "v" or mode == "\22" then
    -- Visual mode: use the common visual range function
    local visual_commands = require("claudecode.visual_commands")
    local start_line, end_line = visual_commands.get_visual_range()

    -- Get current directory once
    local dir_ok, current_dir = pcall(oil.get_current_dir, bufnr)
    if not dir_ok or not current_dir then
      return {}, "Failed to get current directory"
    end

    -- Process each line in the visual selection
    for line = start_line, end_line 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
        if entry.name ~= ".." and entry.name ~= "." then
          local full_path = current_dir .. entry.name
          -- Handle various entry types
          if entry.type == "file" or entry.type == "link" then
            table.insert(files, full_path)
          elseif entry.type == "directory" then
            -- Ensure directory paths end with /
            table.insert(files, full_path:match("/$") and full_path or full_path .. "/")
          else
            -- For unknown types, return the path anyway
            table.insert(files, full_path)
          end
        end
      end
    end

    if #files > 0 then
      return files, nil
    end
  else
    -- Normal mode: get file under cursor with error handling
    local ok, entry = pcall(oil.get_cursor_entry)
    if not ok or not entry then
      return {}, "Failed to get cursor entry"
    end

    local dir_ok, current_dir = pcall(oil.get_current_dir, bufnr)
    if not dir_ok or not current_dir then
      return {}, "Failed to get current directory"
    end

    -- Process the entry
    if entry.name and entry.name ~= ".." and entry.name ~= "." then
      local full_path = current_dir .. entry.name
      -- Handle various entry types
      if entry.type == "file" or entry.type == "link" then
        return { full_path }, nil
      elseif entry.type == "directory" then
        -- Ensure directory paths end with /
        return { full_path:match("/$") and full_path or full_path .. "/" }, nil
      else
        -- For unknown types, return the path anyway
        return { full_path }, nil
      end
    end
  end

  return {}, "No file found under cursor"
end

return M

--- Manages configuration for the Claude Code Neovim integration.
-- Provides default settings, validation, and application of user-defined configurations.
local M = {}

M.defaults = {
  port_range = { min = 10000, max = 65535 },
  auto_start = true,
  terminal_cmd = nil,
  log_level = "info",
  track_selection = true,
  visual_demotion_delay_ms = 50, -- Milliseconds to wait before demoting a visual selection
  connection_wait_delay = 200, -- Milliseconds to wait after connection before sending queued @ mentions
  connection_timeout = 10000, -- Maximum time to wait for Claude Code to connect (milliseconds)
  queue_timeout = 5000, -- Maximum time to keep @ mentions in queue (milliseconds)
  diff_opts = {
    auto_close_on_accept = true,
    show_diff_stats = true,
    vertical_split = true,
    open_in_current_tab = true, -- Use current tab instead of creating new tab
  },
}

--- Validates the provided configuration table.
-- @param config table The configuration table to validate.
-- @return boolean true if the configuration is valid.
-- @error string if any configuration option is invalid.
function M.validate(config)
  assert(
    type(config.port_range) == "table"
      and type(config.port_range.min) == "number"
      and type(config.port_range.max) == "number"
      and config.port_range.min > 0
      and config.port_range.max <= 65535
      and config.port_range.min <= config.port_range.max,
    "Invalid port range"
  )

  assert(type(config.auto_start) == "boolean", "auto_start must be a boolean")

  assert(config.terminal_cmd == nil or type(config.terminal_cmd) == "string", "terminal_cmd must be nil or a string")

  local valid_log_levels = { "trace", "debug", "info", "warn", "error" }
  local is_valid_log_level = false
  for _, level in ipairs(valid_log_levels) do
    if config.log_level == level then
      is_valid_log_level = true
      break
    end
  end
  assert(is_valid_log_level, "log_level must be one of: " .. table.concat(valid_log_levels, ", "))

  assert(type(config.track_selection) == "boolean", "track_selection must be a boolean")

  assert(
    type(config.visual_demotion_delay_ms) == "number" and config.visual_demotion_delay_ms >= 0,
    "visual_demotion_delay_ms must be a non-negative number"
  )

  assert(
    type(config.connection_wait_delay) == "number" and config.connection_wait_delay >= 0,
    "connection_wait_delay must be a non-negative number"
  )

  assert(
    type(config.connection_timeout) == "number" and config.connection_timeout > 0,
    "connection_timeout must be a positive number"
  )

  assert(type(config.queue_timeout) == "number" and config.queue_timeout > 0, "queue_timeout must be a positive number")

  assert(type(config.diff_opts) == "table", "diff_opts must be a table")
  assert(type(config.diff_opts.auto_close_on_accept) == "boolean", "diff_opts.auto_close_on_accept must be a boolean")
  assert(type(config.diff_opts.show_diff_stats) == "boolean", "diff_opts.show_diff_stats must be a boolean")
  assert(type(config.diff_opts.vertical_split) == "boolean", "diff_opts.vertical_split must be a boolean")
  assert(type(config.diff_opts.open_in_current_tab) == "boolean", "diff_opts.open_in_current_tab must be a boolean")

  return true
end

--- Applies user configuration on top of default settings and validates the result.
-- @param user_config table|nil The user-provided configuration table.
-- @return table The final, validated configuration table.
function M.apply(user_config)
  local config = vim.deepcopy(M.defaults)

  if user_config then
    config = vim.tbl_deep_extend("force", config, user_config)
  end

  M.validate(config)

  return config
end

return M

---@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

-- Development configuration for claudecode.nvim
-- This is Thomas's personal config for developing claudecode.nvim
-- Symlink this to your personal Neovim config:
-- ln -s ~/projects/claudecode.nvim/dev-config.lua ~/.config/nvim/lua/plugins/dev-claudecode.lua

return {
  "coder/claudecode.nvim",
  dev = true, -- Use local development version
  keys = {
    -- AI/Claude Code prefix
    { "<leader>a", nil, desc = "AI/Claude Code" },

    -- Core Claude commands
    { "<leader>ac", "<cmd>ClaudeCode<cr>", desc = "Toggle Claude" },
    { "<leader>af", "<cmd>ClaudeCodeFocus<cr>", desc = "Focus Claude" },
    { "<leader>ar", "<cmd>ClaudeCode --resume<cr>", desc = "Resume Claude" },
    { "<leader>aC", "<cmd>ClaudeCode --continue<cr>", desc = "Continue Claude" },

    -- Context sending
    { "<leader>ab", "<cmd>ClaudeCodeAdd %<cr>", desc = "Add current buffer" },
    { "<leader>as", "<cmd>ClaudeCodeSend<cr>", mode = "v", desc = "Send to Claude" },
    {
      "<leader>as",
      "<cmd>ClaudeCodeTreeAdd<cr>",
      desc = "Add file from tree",
      ft = { "NvimTree", "neo-tree", "oil" },
    },

    -- Development helpers
    { "<leader>ao", "<cmd>ClaudeCodeOpen<cr>", desc = "Open Claude" },
    { "<leader>aq", "<cmd>ClaudeCodeClose<cr>", desc = "Close Claude" },
    { "<leader>ai", "<cmd>ClaudeCodeStatus<cr>", desc = "Claude Status" },
    { "<leader>aS", "<cmd>ClaudeCodeStart<cr>", desc = "Start Claude Server" },
    { "<leader>aQ", "<cmd>ClaudeCodeStop<cr>", desc = "Stop Claude Server" },

    -- Diff management (buffer-local, only active in diff buffers)
    { "<leader>aa", "<cmd>ClaudeCodeDiffAccept<cr>", desc = "Accept diff" },
    { "<leader>ad", "<cmd>ClaudeCodeDiffDeny<cr>", desc = "Deny diff" },
  },

  -- Development configuration - all options shown with defaults commented out
  opts = {
    -- Server Configuration
    -- port_range = { min = 10000, max = 65535 },  -- WebSocket server port range
    -- auto_start = true,                          -- Auto-start server on Neovim startup
    -- log_level = "info",                         -- "trace", "debug", "info", "warn", "error"
    -- terminal_cmd = nil,                         -- Custom terminal command (default: "claude")

    -- Selection Tracking
    -- track_selection = true,                     -- Enable real-time selection tracking
    -- visual_demotion_delay_ms = 50,             -- Delay before demoting visual selection (ms)

    -- Connection Management
    -- connection_wait_delay = 200,                -- Wait time after connection before sending queued @ mentions (ms)
    -- connection_timeout = 10000,                 -- Max time to wait for Claude Code connection (ms)
    -- queue_timeout = 5000,                       -- Max time to keep @ mentions in queue (ms)

    -- Diff Integration
    -- diff_opts = {
    --   auto_close_on_accept = true,              -- Close diff view after accepting changes
    --   show_diff_stats = true,                   -- Show diff statistics
    --   vertical_split = true,                    -- Use vertical split for diffs
    --   open_in_current_tab = true,               -- Open diffs in current tab vs new tab
    -- },

    -- Terminal Configuration
    -- terminal = {
    --   split_side = "right",                     -- "left" or "right"
    --   split_width_percentage = 0.30,            -- Width as percentage (0.0 to 1.0)
    --   provider = "auto",                        -- "auto", "snacks", or "native"
    --   show_native_term_exit_tip = true,         -- Show exit tip for native terminal
    --   auto_close = true,                        -- Auto-close terminal after command completion
    -- },

    -- Development overrides (uncomment as needed)
    -- log_level = "debug",
    -- terminal = {
    --   provider = "native",
    --   auto_close = false, -- Keep terminals open to see output
    -- },
  },
}

---@brief Centralized logger for Claude Code Neovim integration.
-- Provides level-based logging.
local M = {}

M.levels = {
  ERROR = 1,
  WARN = 2,
  INFO = 3,
  DEBUG = 4,
  TRACE = 5,
}

local level_values = {
  error = M.levels.ERROR,
  warn = M.levels.WARN,
  info = M.levels.INFO,
  debug = M.levels.DEBUG,
  trace = M.levels.TRACE,
}

local current_log_level_value = M.levels.INFO

--- @param plugin_config table The configuration table (e.g., from claudecode.init.state.config).
function M.setup(plugin_config)
  local conf = plugin_config

  if conf and conf.log_level and level_values[conf.log_level] then
    current_log_level_value = level_values[conf.log_level]
  else
    vim.notify(
      "ClaudeCode Logger: Invalid or missing log_level in configuration (received: "
        .. tostring(conf and conf.log_level)
        .. "). Defaulting to INFO.",
      vim.log.levels.WARN
    )
    current_log_level_value = M.levels.INFO
  end
end

local function log(level, component, message_parts)
  if level > current_log_level_value then
    return
  end

  local prefix = "[ClaudeCode]"
  if component then
    prefix = prefix .. " [" .. component .. "]"
  end

  local level_name = "UNKNOWN"
  for name, val in pairs(M.levels) do
    if val == level then
      level_name = name
      break
    end
  end
  prefix = prefix .. " [" .. level_name .. "]"

  local message = ""
  for i, part in ipairs(message_parts) do
    if i > 1 then
      message = message .. " "
    end
    if type(part) == "table" or type(part) == "boolean" then
      message = message .. vim.inspect(part)
    else
      message = message .. tostring(part)
    end
  end

  if level == M.levels.ERROR then
    vim.schedule(function()
      vim.notify(prefix .. " " .. message, vim.log.levels.ERROR, { title = "ClaudeCode Error" })
    end)
  elseif level == M.levels.WARN then
    vim.schedule(function()
      vim.notify(prefix .. " " .. message, vim.log.levels.WARN, { title = "ClaudeCode Warning" })
    end)
  else
    -- For INFO, DEBUG, TRACE, use nvim_echo to avoid flooding notifications,
    -- to make them appear in :messages, and wrap in vim.schedule
    -- to avoid "nvim_echo must not be called in a fast event context".
    vim.schedule(function()
      vim.api.nvim_echo({ { prefix .. " " .. message, "Normal" } }, true, {})
    end)
  end
end

--- @param component string|nil Optional component/module name.
-- @param ... any Varargs representing parts of the message.
function M.error(component, ...)
  if type(component) ~= "string" then
    log(M.levels.ERROR, nil, { component, ... })
  else
    log(M.levels.ERROR, component, { ... })
  end
end

--- @param component string|nil Optional component/module name.
-- @param ... any Varargs representing parts of the message.
function M.warn(component, ...)
  if type(component) ~= "string" then
    log(M.levels.WARN, nil, { component, ... })
  else
    log(M.levels.WARN, component, { ... })
  end
end

--- @param component string|nil Optional component/module name.
-- @param ... any Varargs representing parts of the message.
function M.info(component, ...)
  if type(component) ~= "string" then
    log(M.levels.INFO, nil, { component, ... })
  else
    log(M.levels.INFO, component, { ... })
  end
end

--- Check if a specific log level is enabled
-- @param level_name string The level name ("error", "warn", "info", "debug", "trace")
-- @return boolean Whether the level is enabled
function M.is_level_enabled(level_name)
  local level_value = level_values[level_name]
  if not level_value then
    return false
  end
  return level_value <= current_log_level_value
end

--- @param component string|nil Optional component/module name.
-- @param ... any Varargs representing parts of the message.
function M.debug(component, ...)
  if type(component) ~= "string" then
    log(M.levels.DEBUG, nil, { component, ... })
  else
    log(M.levels.DEBUG, component, { ... })
  end
end

--- @param component string|nil Optional component/module name.
-- @param ... any Varargs representing parts of the message.
function M.trace(component, ...)
  if type(component) ~= "string" then
    log(M.levels.TRACE, nil, { component, ... })
  else
    log(M.levels.TRACE, component, { ... })
  end
end

local default_config_for_initial_setup = require("claudecode.config").defaults
M.setup(default_config_for_initial_setup)

return M

---@meta vim_api_definitions
-- This file provides type definitions for parts of the Neovim API
-- to help the Lua language server (LuaLS) with diagnostics.

---@class vim_log_levels
---@field NONE number
---@field ERROR number
---@field WARN number
---@field INFO number
---@field DEBUG number
---@field TRACE number

---@class vim_log
---@field levels vim_log_levels

---@class vim_notify_opts
---@field title string|nil
---@field icon string|nil
---@field on_open fun(winid: number)|nil
---@field on_close fun()|nil
---@field timeout number|nil
---@field keep fun()|nil
---@field plugin string|nil
---@field hide_from_history boolean|nil
---@field once boolean|nil
---@field on_close_timeout number|nil

---@class vim_options_table: table<string, any>
---@field columns number Global option: width of the screen
---@field lines number Global option: height of the screen
-- Add other commonly used vim.o options as needed

---@class vim_buffer_options_table: table<string, any>

---@class vim_bo_proxy: vim_buffer_options_table
---@field __index fun(self: vim_bo_proxy, bufnr: number): vim_buffer_options_table Allows vim.bo[bufnr]

---@class vim_diagnostic_info
---@field bufnr number
---@field col number
---@field end_col number|nil
---@field end_lnum number|nil
---@field lnum number
---@field message string
---@field severity number
---@field source string|nil
---@field user_data any|nil

---@class vim_diagnostic_module
---@field get fun(bufnr?: number, ns_id?: number): vim_diagnostic_info[]
-- Add other vim.diagnostic functions as needed, e.g., get_namespace, set, etc.

---@class vim_fs_module
---@field remove fun(path: string, opts?: {force?: boolean, recursive?: boolean}):boolean|nil

---@class vim_filetype_module
---@field match fun(args: {filename: string, contents?: string}):string|nil

---@class vim_fn_table
---@field mode fun(mode_str?: string, full?: boolean|number):string
---@field delete fun(name: string, flags?: string):integer For file deletion
---@field filereadable fun(file: string):integer
---@field fnamemodify fun(fname: string, mods: string):string
---@field expand fun(str: string, ...):string|table
---@field getcwd fun(winid?: number, tabnr?: number):string
---@field mkdir fun(name: string, path?: string, prot?: number):integer
---@field buflisted fun(bufnr: number|string):integer
---@field bufname fun(expr?: number|string):string
---@field bufnr fun(expr?: string|number, create?: boolean):number
---@field win_getid fun(win?: number, tab?: number):number
---@field win_gotoid fun(winid: number):boolean
---@field line fun(expr: string, winid?: number):number
---@field col fun(expr: string, winid?: number):number
---@field virtcol fun(expr: string|string[], winid?: number):number|number[]
---@field getpos fun(expr: string, winid?: number):number[]
---@field setpos fun(expr: string, pos: number[], winid?: number):boolean
---@field tempname fun():string
---@field globpath fun(path: string, expr: string, ...):string
---@field stdpath fun(type: "cache"|"config"|"data"|"log"|"run"|"state"|"config_dirs"|"data_dirs"):string|string[]
---@field json_encode fun(expr: any):string
---@field json_decode fun(string: string, opts?: {null_value?: any}):any
---@field termopen fun(cmd: string|string[], opts?: table):number For vim.fn.termopen()
-- Add other vim.fn functions as needed

---@class vim_v_table
---@field event table Event data containing status and other event information

---@class vim_global_api
---@field notify fun(msg: string | string[], level?: number, opts?: vim_notify_opts):nil
---@field log vim_log
---@field v vim_v_table For vim.v.event access
---@field _last_echo table[]? table of tables, e.g. { {"message", "HighlightGroup"} }
---@field _last_error string?
---@field o vim_options_table For vim.o.option_name
---@field bo vim_bo_proxy      For vim.bo.option_name and vim.bo[bufnr].option_name
---@field diagnostic vim_diagnostic_module For vim.diagnostic.*
---@field empty_dict fun(): table For vim.empty_dict()
---@field schedule_wrap fun(fn: function): function For vim.schedule_wrap()
---@field deepcopy fun(val: any): any For vim.deepcopy() -- Added based on test mocks
---@field _current_mode string? For mocks in tests
---@class vim_api_table
---@field nvim_create_augroup fun(name: string, opts: {clear: boolean}):integer
---@field nvim_create_autocmd fun(event: string|string[], opts: {group?: string|integer, pattern?: string|string[], buffer?: number, callback?: function|string, once?: boolean, desc?: string}):integer
---@field nvim_clear_autocmds fun(opts: {group?: string|integer, event?: string|string[], pattern?: string|string[], buffer?: number}):nil
---@field nvim_get_current_buf fun():integer
---@field nvim_get_mode fun():{mode: string, blocking: boolean}
---@field nvim_win_get_cursor fun(window: integer):integer[] Returns [row, col] (1-based for row, 0-based for col)
---@field nvim_buf_get_name fun(buffer: integer):string
---@field nvim_buf_get_lines fun(buffer: integer, start: integer, end_line: integer, strict_indexing: boolean):string[]
-- Add other nvim_api functions as needed
---@field cmd fun(command: string):nil For vim.cmd() -- Added based on test mocks
---@field api vim_api_table For vim.api.*
---@field fn vim_fn_table For vim.fn.*
---@field fs vim_fs_module For vim.fs.*
---@field filetype vim_filetype_module For vim.filetype.*
---@field test vim_test_utils? For test utility mocks
---@field split fun(str: string, pat?: string, opts?: {plain?: boolean, trimempty?: boolean}):string[] For vim.split()
-- Add other vim object definitions here if they cause linting issues
-- e.g. vim.api, vim.loop, vim.deepcopy, etc.

---@class SpyCall
---@field vals table[] table of arguments passed to the call
---@field self any the 'self' object for the call if it was a method

---@class SpyInformation
---@field calls SpyCall[] A list of calls made to the spy.
---@field call_count number The number of times the spy has been called.
-- Add other spy properties if needed e.g. returned, threw

---@class SpyAsserts
---@field was_called fun(self: SpyAsserts, count?: number):boolean
---@field was_called_with fun(self: SpyAsserts, ...):boolean
---@field was_not_called fun(self: SpyAsserts):boolean
-- Add other spy asserts if needed

---@class SpyableFunction : function
---@field __call fun(self: SpyableFunction, ...):any
---@field spy fun(self: SpyableFunction):SpyAsserts Returns an assertion object for the spy.
---@field calls SpyInformation[]? Information about calls made to the spied function.
-- Note: In some spy libraries, 'calls' might be directly on the spied function,
-- or on an object returned by `spy()`. Adjust as per your spy library's specifics.
-- For busted's default spy, `calls` is often directly on the spied function.

---@class vim_test_utils
---@field add_buffer fun(bufnr: number, filename: string, content: string|string[]):nil
---@field set_cursor fun(bufnr: number, row: number, col: number):nil
-- Add other test utility functions as needed

-- This section helps LuaLS understand that 'vim' is a global variable
-- with the structure defined above. It's for type hinting only and
-- does not execute or overwrite the actual 'vim' global provided by Neovim.
---@type vim_global_api

---@brief [[
--- Lock file management for Claude Code Neovim integration.
--- This module handles creation, removal and updating of lock files
--- which allow Claude Code CLI to discover the Neovim integration.
---@brief ]]

local M = {}

--- Path to the lock file directory
---@return string lock_dir The path to the lock file directory
local function get_lock_dir()
  local claude_config_dir = os.getenv("CLAUDE_CONFIG_DIR")
  if claude_config_dir and claude_config_dir ~= "" then
    return vim.fn.expand(claude_config_dir .. "/ide")
  else
    return vim.fn.expand("~/.claude/ide")
  end
end

M.lock_dir = get_lock_dir()

-- Track if random seed has been initialized
local random_initialized = false

--- Generate a random UUID for authentication
---@return string uuid A randomly generated UUID string
local function generate_auth_token()
  -- Initialize random seed only once
  if not random_initialized then
    local seed = os.time() + vim.fn.getpid()
    -- Add more entropy if available
    if vim.loop and vim.loop.hrtime then
      seed = seed + (vim.loop.hrtime() % 1000000)
    end
    math.randomseed(seed)

    -- Call math.random a few times to "warm up" the generator
    for _ = 1, 10 do
      math.random()
    end
    random_initialized = true
  end

  -- Generate UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
  local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
  local uuid = template:gsub("[xy]", function(c)
    local v = (c == "x") and math.random(0, 15) or math.random(8, 11)
    return string.format("%x", v)
  end)

  -- Validate generated UUID format
  if not uuid:match("^[0-9a-f]+-[0-9a-f]+-[0-9a-f]+-[0-9a-f]+-[0-9a-f]+$") then
    error("Generated invalid UUID format: " .. uuid)
  end

  if #uuid ~= 36 then
    error("Generated UUID has invalid length: " .. #uuid .. " (expected 36)")
  end

  return uuid
end

--- Generate a new authentication token
---@return string auth_token A newly generated authentication token
function M.generate_auth_token()
  return generate_auth_token()
end

--- Create the lock file for a specified WebSocket port
---@param port number The port number for the WebSocket server
---@param auth_token string|nil Optional pre-generated auth token (generates new one if not provided)
---@return boolean success Whether the operation was successful
---@return string result_or_error The lock file path if successful, or error message if failed
---@return string? auth_token The authentication token if successful
function M.create(port, auth_token)
  if not port or type(port) ~= "number" then
    return false, "Invalid port number"
  end

  if port < 1 or port > 65535 then
    return false, "Port number out of valid range (1-65535): " .. tostring(port)
  end

  local ok, err = pcall(function()
    return vim.fn.mkdir(M.lock_dir, "p")
  end)

  if not ok then
    return false, "Failed to create lock directory: " .. (err or "unknown error")
  end

  local lock_path = M.lock_dir .. "/" .. port .. ".lock"

  local workspace_folders = M.get_workspace_folders()
  if not auth_token then
    local auth_success, auth_result = pcall(generate_auth_token)
    if not auth_success then
      return false, "Failed to generate authentication token: " .. (auth_result or "unknown error")
    end
    auth_token = auth_result
  else
    -- Validate provided auth_token
    if type(auth_token) ~= "string" then
      return false, "Authentication token must be a string, got " .. type(auth_token)
    end
    if #auth_token < 10 then
      return false, "Authentication token too short (minimum 10 characters)"
    end
    if #auth_token > 500 then
      return false, "Authentication token too long (maximum 500 characters)"
    end
  end

  -- Prepare lock file content
  local lock_content = {
    pid = vim.fn.getpid(),
    workspaceFolders = workspace_folders,
    ideName = "Neovim",
    transport = "ws",
    authToken = auth_token,
  }

  local json
  local ok_json, json_err = pcall(function()
    json = vim.json.encode(lock_content)
    return json
  end)

  if not ok_json or not json then
    return false, "Failed to encode lock file content: " .. (json_err or "unknown error")
  end

  local file = io.open(lock_path, "w")
  if not file then
    return false, "Failed to create lock file: " .. lock_path
  end

  local write_ok, write_err = pcall(function()
    file:write(json)
    file:close()
  end)

  if not write_ok then
    pcall(function()
      file:close()
    end)
    return false, "Failed to write lock file: " .. (write_err or "unknown error")
  end

  return true, lock_path, auth_token
end

--- Remove the lock file for the given port
---@param port number The port number of the WebSocket server
---@return boolean success Whether the operation was successful
---@return string? error Error message if operation failed
function M.remove(port)
  if not port or type(port) ~= "number" then
    return false, "Invalid port number"
  end

  local lock_path = M.lock_dir .. "/" .. port .. ".lock"

  if vim.fn.filereadable(lock_path) == 0 then
    return false, "Lock file does not exist: " .. lock_path
  end

  local ok, err = pcall(function()
    return os.remove(lock_path)
  end)

  if not ok then
    return false, "Failed to remove lock file: " .. (err or "unknown error")
  end

  return true
end

--- Update the lock file for the given port
---@param port number The port number of the WebSocket server
---@return boolean success Whether the operation was successful
---@return string result_or_error The lock file path if successful, or error message if failed
---@return string? auth_token The authentication token if successful
function M.update(port)
  if not port or type(port) ~= "number" then
    return false, "Invalid port number"
  end

  local exists = vim.fn.filereadable(M.lock_dir .. "/" .. port .. ".lock") == 1
  if exists then
    local remove_ok, remove_err = M.remove(port)
    if not remove_ok then
      return false, "Failed to update lock file: " .. remove_err
    end
  end

  return M.create(port)
end

--- Read the authentication token from a lock file
---@param port number The port number of the WebSocket server
---@return boolean success Whether the operation was successful
---@return string? auth_token The authentication token if successful, or nil if failed
---@return string? error Error message if operation failed
function M.get_auth_token(port)
  if not port or type(port) ~= "number" then
    return false, nil, "Invalid port number"
  end

  local lock_path = M.lock_dir .. "/" .. port .. ".lock"

  if vim.fn.filereadable(lock_path) == 0 then
    return false, nil, "Lock file does not exist: " .. lock_path
  end

  local file = io.open(lock_path, "r")
  if not file then
    return false, nil, "Failed to open lock file: " .. lock_path
  end

  local content = file:read("*all")
  file:close()

  if not content or content == "" then
    return false, nil, "Lock file is empty: " .. lock_path
  end

  local ok, lock_data = pcall(vim.json.decode, content)
  if not ok or type(lock_data) ~= "table" then
    return false, nil, "Failed to parse lock file JSON: " .. lock_path
  end

  local auth_token = lock_data.authToken
  if not auth_token or type(auth_token) ~= "string" then
    return false, nil, "No valid auth token found in lock file"
  end

  return true, auth_token, nil
end

--- Get active LSP clients using available API
---@return table Array of LSP clients
local function get_lsp_clients()
  if vim.lsp then
    if vim.lsp.get_clients then
      -- Neovim >= 0.11
      return vim.lsp.get_clients()
    elseif vim.lsp.get_active_clients then
      -- Neovim 0.8-0.10
      return vim.lsp.get_active_clients()
    end
  end
  return {}
end

--- Get workspace folders for the lock file
---@return table Array of workspace folder paths
function M.get_workspace_folders()
  local folders = {}

  -- Add current working directory
  table.insert(folders, vim.fn.getcwd())

  -- Get LSP workspace folders if available
  local clients = get_lsp_clients()
  for _, client in pairs(clients) do
    if client.config and client.config.workspace_folders then
      for _, ws in ipairs(client.config.workspace_folders) do
        -- Convert URI to path
        local path = ws.uri
        if path:sub(1, 7) == "file://" then
          path = path:sub(8)
        end

        -- Check if already in the list
        local exists = false
        for _, folder in ipairs(folders) do
          if folder == path then
            exists = true
            break
          end
        end

        if not exists then
          table.insert(folders, path)
        end
      end
    end
  end

  return folders
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

if vim.fn.has("nvim-0.8.0") ~= 1 then
  vim.api.nvim_err_writeln("Claude Code requires Neovim >= 0.8.0")
  return
end

if vim.g.loaded_claudecode then
  return
end
vim.g.loaded_claudecode = 1

--- Example: In your `init.lua`, you can set `vim.g.claudecode_auto_setup = { auto_start = true }`
--- to automatically start ClaudeCode when Neovim loads.
if vim.g.claudecode_auto_setup then
  vim.defer_fn(function()
    require("claudecode").setup(vim.g.claudecode_auto_setup)
  end, 0)
end

-- Commands are now registered in lua/claudecode/init.lua's _create_commands function
-- when require("claudecode").setup() is called.
-- This file (plugin/claudecode.lua) is primarily for the load guard
-- and the optional auto-setup mechanism.

local main_module_ok, _ = pcall(require, "claudecode")
if not main_module_ok then
  vim.notify("ClaudeCode: Failed to load main module. Plugin may not function correctly.", vim.log.levels.ERROR)
end

---@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

---@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

--- Shared utility functions for claudecode.nvim
-- @module claudecode.utils

local M = {}

--- Normalizes focus parameter to default to true for backward compatibility
--- @param focus boolean|nil The focus parameter
--- @return boolean Normalized focus value
function M.normalize_focus(focus)
  return focus == nil and true or focus
end

return M

This code segment functions to: Manage terminal window lifecycle by checking existing instances, toggling visibility states, and handling focus transitions while providing error handling for initialization failures.

```lua
[TASK_BEGIN]
function M.open(cmd_string, env_table, config, focus)
  if not is_available() then
    vim.notify("Snacks.nvim terminal provider selected but Snacks.terminal not available.", vim.log.levels.ERROR)
    return
  end

  local logger = require("claudecode.logger")

  -- Reuse existing terminal if possible
  if terminal and terminal:buf_valid() then
    if not terminal.win then  -- Exists but not visible
      logger.debug("terminal", "Showing existing terminal")
      terminal:toggle()
    end

    if focus then
      vim.api.nvim_set_current_win(terminal.win)
    end
    return
  end

  -- Create new terminal instance
  logger.debug("terminal", "Launching new terminal")
  local opts = build_opts(config, env_table, focus)
  local term_instance = Snacks.terminal.new(cmd_string, opts)

  setup_terminal_events(term_instance, config)
  terminal = term_instance
end
[TASK_END]
```

function M.open(cmd_string, env_table, config, focus)
  if not is_available() then
    vim.notify("Snacks.nvim terminal provider selected but Snacks.terminal not available.", vim.log.levels.ERROR)
    return
  end
  local logger = require("claudecode.logger")
  if terminal and terminal:buf_valid() then
    if not terminal.win then
      logger.debug("terminal", "Showing existing terminal")
      terminal:toggle()
    end
    if focus then
      vim.api.nvim_set_current_win(terminal.win)
    end
    return
  end
  logger.debug("terminal", "Launching new terminal")
  local opts = build_opts(config, env_table, focus)
  local term_instance = Snacks.terminal.new(cmd_string, opts)
  setup_terminal_events(term_instance, config)
  terminal = term_instance
end